Explorar el Código

iso格式的提取dsf的音轨 终于可以提取成功了(缺点速度慢)
增加心电图频谱和水波纹频谱
增加PlayingIndicator正在播放的动画指示器

onecold hace 4 meses
padre
commit
f3eaa27b6a
Se han modificado 39 ficheros con 3807 adiciones y 372 borrados
  1. 442 0
      doc/SACD_DST最小接入方案.md
  2. 65 0
      entry/src/main/cpp/napi_init.cpp
  3. 27 0
      entry/src/main/cpp/third_party/sacd/libdstdec/CMakeLists.txt
  4. 152 0
      entry/src/main/cpp/third_party/sacd/libdstdec/ac.h
  5. 132 0
      entry/src/main/cpp/third_party/sacd/libdstdec/binding/dst_decoder_mt.cpp
  6. 87 0
      entry/src/main/cpp/third_party/sacd/libdstdec/binding/dst_decoder_mt.h
  7. 43 0
      entry/src/main/cpp/third_party/sacd/libdstdec/binding/semaphore.h
  8. 95 0
      entry/src/main/cpp/third_party/sacd/libdstdec/common.h
  9. 47 0
      entry/src/main/cpp/third_party/sacd/libdstdec/consts.h
  10. 84 0
      entry/src/main/cpp/third_party/sacd/libdstdec/ct.h
  11. 291 0
      entry/src/main/cpp/third_party/sacd/libdstdec/decoder/decoder.cpp
  12. 56 0
      entry/src/main/cpp/third_party/sacd/libdstdec/decoder/decoder.h
  13. 78 0
      entry/src/main/cpp/third_party/sacd/libdstdec/fh.h
  14. 419 0
      entry/src/main/cpp/third_party/sacd/libdstdec/fr.h
  15. 36 0
      entry/src/main/cpp/third_party/sacd/libdstdec/segment.h
  16. 79 0
      entry/src/main/cpp/third_party/sacd/libdstdec/stream.h
  17. 1 7
      entry/src/main/ets/common/constants/CommonConstants.ets
  18. 1 0
      entry/src/main/ets/common/enums/RemoteDriveManagerStates.ets
  19. 1 3
      entry/src/main/ets/common/util/BackgroundManager.ets
  20. 494 77
      entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets
  21. 5 0
      entry/src/main/ets/common/util/IsoArchiveUtil.ets
  22. 122 27
      entry/src/main/ets/common/util/RcpSocketUtil.ets
  23. 54 0
      entry/src/main/ets/common/util/SearchHistoryUtil.ets
  24. 66 53
      entry/src/main/ets/pages/PlaylistDetailPage.ets
  25. 5 0
      entry/src/main/ets/pages/SettingPage.ets
  26. 23 13
      entry/src/main/ets/pages/UploadMusicPage.ets
  27. 8 12
      entry/src/main/ets/pages/WebDavMainPage.ets
  28. 118 29
      entry/src/main/ets/view/IsoExtractComptent.ets
  29. 295 123
      entry/src/main/ets/view/LocalMusic.ets
  30. 153 0
      entry/src/main/ets/view/PlayingIndicator.ets
  31. 38 26
      entry/src/main/ets/view/RemoteMusicPage.ets
  32. 141 0
      entry/src/main/ets/view/spectrum/EcgSpectrumRenderer.ets
  33. 131 0
      entry/src/main/ets/view/spectrum/RippleWaveSpectrumRenderer.ets
  34. 16 0
      entry/src/main/ets/view/spectrum/SpectrumEffectRegistry.ets
  35. 2 2
      entry/src/main/ets/viewmodel/MainViewModel.ets
  36. BIN
      entry/src/main/resources/base/media/app_loading0.png
  37. BIN
      entry/src/main/resources/base/media/app_loading1.png
  38. BIN
      entry/src/main/resources/base/media/app_loading2.png
  39. BIN
      entry/src/main/resources/base/media/app_loading3.png

+ 442 - 0
doc/SACD_DST最小接入方案.md

@@ -0,0 +1,442 @@
+# 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` 里包含了 `<kodi/General.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<dst_decoder_t> decoder;
+    std::vector<uint8_t> 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` 分支接上去
+

+ 65 - 0
entry/src/main/cpp/napi_init.cpp

@@ -74,6 +74,7 @@ struct SacdDstDecoderSession {
     size_t dsdFrameSize = 0;
     std::unique_ptr<dst::decoder_t> decoder;
     std::vector<uint8_t> decodeBuffer;
+    std::vector<uint8_t> batchDecodeBuffer;
     std::mutex decodeMutex;
 };
 
@@ -1053,6 +1054,69 @@ napi_value DecodeSacdDstFrame(napi_env env, napi_callback_info info)
     }
 }
 
+napi_value DecodeSacdDstFrames(napi_env env, napi_callback_info info)
+{
+    try {
+        size_t argc = 3;
+        napi_value args[3] = {nullptr};
+        NapiCheck(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr), "Failed to get arguments for decodeSacdDstFrames");
+        if (argc < 3) {
+            throw std::runtime_error("decodeSacdDstFrames requires decoderId, payload and frameLengths");
+        }
+
+        int64_t decoderId = ReadInt64(env, args[0], "decoderId");
+        ArrayBufferInfo payload = ReadArrayBuffer(env, args[1], "payload");
+        ArrayBufferInfo frameLengthsBuffer = ReadArrayBuffer(env, args[2], "frameLengths");
+        Ensure(payload.data != nullptr && payload.length > 0, "payload must not be empty");
+        Ensure(frameLengthsBuffer.data != nullptr && frameLengthsBuffer.length >= sizeof(uint32_t),
+            "frameLengths must not be empty");
+        Ensure(frameLengthsBuffer.length % sizeof(uint32_t) == 0,
+            "frameLengths byte length must be a multiple of 4");
+
+        const auto *frameLengths = reinterpret_cast<const uint32_t *>(frameLengthsBuffer.data);
+        const size_t frameCount = frameLengthsBuffer.length / sizeof(uint32_t);
+        Ensure(frameCount > 0, "frameLengths must contain at least one frame");
+
+        size_t totalPayloadBytes = 0;
+        for (size_t index = 0; index < frameCount; ++index) {
+            const size_t frameLength = static_cast<size_t>(frameLengths[index]);
+            Ensure(frameLength > 0, "frameLengths must not contain empty frames");
+            Ensure(totalPayloadBytes + frameLength >= totalPayloadBytes,
+                "frameLengths total size overflow");
+            totalPayloadBytes += frameLength;
+        }
+        Ensure(totalPayloadBytes == payload.length,
+            "payload size does not match frameLengths total size");
+
+        auto session = RequireSacdDstDecoderSession(decoderId);
+        std::lock_guard<std::mutex> decodeLock(session->decodeMutex);
+        const size_t totalDecodedBytes = frameCount * session->dsdFrameSize;
+        Ensure(totalDecodedBytes > 0, "Computed decoded batch size is invalid");
+        session->batchDecodeBuffer.resize(totalDecodedBytes);
+
+        size_t payloadOffset = 0;
+        for (size_t index = 0; index < frameCount; ++index) {
+            const size_t frameLength = static_cast<size_t>(frameLengths[index]);
+            uint8_t *decodedData = session->batchDecodeBuffer.data() + (index * session->dsdFrameSize);
+            const int rc = session->decoder->decode(
+                payload.data + payloadOffset,
+                static_cast<unsigned int>(frameLength * 8),
+                decodedData
+            );
+            if (rc != 0) {
+                SACD_DST_LOGE("batch decode failed id=%{public}lld rc=%{public}d frameIndex=%{public}zu frameSize=%{public}zu output=%{public}zu",
+                    static_cast<long long>(decoderId), rc, index, frameLength, session->dsdFrameSize);
+                throw std::runtime_error("Failed to decode SACD DST frame batch");
+            }
+            payloadOffset += frameLength;
+        }
+        return CreateArrayBufferCopy(env, session->batchDecodeBuffer.data(), totalDecodedBytes);
+    } catch (const std::exception &error) {
+        napi_throw_error(env, nullptr, error.what());
+        return nullptr;
+    }
+}
+
 napi_value ReleaseSacdDstDecoder(napi_env env, napi_callback_info info)
 {
     try {
@@ -1101,6 +1165,7 @@ static napi_value Init(napi_env env, napi_value exports)
         {"readSmbFileRange", nullptr, ReadSmbFileRange, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"createSacdDstDecoder", nullptr, CreateSacdDstDecoder, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"decodeSacdDstFrame", nullptr, DecodeSacdDstFrame, nullptr, nullptr, nullptr, napi_default, nullptr},
+        {"decodeSacdDstFrames", nullptr, DecodeSacdDstFrames, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"releaseSacdDstDecoder", nullptr, ReleaseSacdDstDecoder, nullptr, nullptr, nullptr, napi_default, nullptr}
     };
     NapiCheck(napi_define_properties(env, exports, sizeof(descriptors) / sizeof(descriptors[0]), descriptors), "Failed to define native exports");

+ 27 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/CMakeLists.txt

@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.5)
+project(libdstdec)
+
+set(CMAKE_CXX_STANDARD 17)
+
+# Include directory paths
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}
+                    ${CMAKE_CURRENT_SOURCE_DIR}/binding
+                    ${CMAKE_CURRENT_SOURCE_DIR}/decoder)
+
+set(SOURCES binding/dst_decoder_mt.cpp
+            decoder/decoder.cpp)
+
+set(HEADERS binding/dst_decoder_mt.h
+            binding/semaphore.h
+            decoder/decoder.h
+            ac.h
+            common.h
+            consts.h
+            ct.h
+            fh.h
+            fr.h
+            segment.h
+            stream.h)
+
+add_library(dstdec STATIC ${SOURCES} ${HEADERS})
+set_property(TARGET dstdec PROPERTY POSITION_INDEPENDENT_CODE ON)

+ 152 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/ac.h

@@ -0,0 +1,152 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef AC_H
+#define AC_H
+
+#include <stdint.h>
+#include "common.h"
+
+namespace dst {
+
+constexpr int PBITS = AC_BITS;       // number of bits for Probabilities
+constexpr int NBITS = 4;             // number of overhead bits: must be at least 2!
+                                     // maximum "variable shift length" is (NBITS-1)
+constexpr int PSUM  = 1 << PBITS;
+constexpr int ABITS = PBITS + NBITS; // must be at least PBITS+2
+constexpr int MB    = 0;             // if (MB) print max buffer use
+constexpr int ONE   = 1 << ABITS;
+constexpr int HALF  = 1 << (ABITS - 1);
+
+class ac_t {
+	unsigned int Init;
+	unsigned int C;
+	unsigned int A;
+	int cbptr;
+public:
+
+	unsigned int getPtableIndex(int PredicVal, int PtableLen) {
+		int j;
+		j = (PredicVal > 0 ? PredicVal : -PredicVal) >> AC_QSTEP;
+		if (j >= PtableLen) {
+			j = PtableLen - 1;
+		}
+		return (unsigned int)j;
+	}
+
+	void decodeBit(uint8_t& b, int p, uint8_t* cb, int fs, int flush) {
+		unsigned int ap;
+		unsigned int h;
+		if (Init == 1) {
+			Init = 0;
+			A = ONE - 1;
+			C = 0;
+			for (cbptr = 1; cbptr <= ABITS; cbptr++) {
+				C <<= 1;
+				if (cbptr < fs) {
+					C |= cb[cbptr];
+				}
+			}
+		}
+		if (flush == 0) {
+			// approximate (A * p) with "partial rounding"
+			ap = ((A >> PBITS) | ((A >> (PBITS - 1)) & 1))* p;
+			h = A - ap;
+			if (C >= h) {
+				b = 0;
+				C -= h;
+				A = ap;
+			}
+			else {
+				b = 1;
+				A = h;
+			}
+			while (A < HALF) {
+				A <<= 1;
+				// Use new flushing technique; insert zero in LSB of C if reading past the end of the arithmetic code
+				C <<= 1;
+				if (cbptr < fs) {
+					C |= cb[cbptr];
+				}
+				cbptr++;
+			}
+		}
+		else {
+			Init = 1;
+			if (cbptr < fs - 7) {
+				b = 0;
+			}
+			else {
+				b = 1;
+				while ((cbptr < fs) && (b == 1)) {
+					if (cb[cbptr] != 0) {
+						b = 1;
+					}
+					cbptr++;
+				}
+			}
+		}
+	}
+
+	void decodeBit_Init(uint8_t* cb, int fs) {
+		Init = 0;
+		A = ONE - 1;
+		C = 0;
+		for (cbptr = 1; cbptr <= ABITS; cbptr++) {
+			C <<= 1;
+			if (cbptr < fs) {
+				C |= GET_BIT(cb, cbptr);
+			}
+		}
+	}
+
+	void decodeBit_Decode(uint8_t* b, int p, uint8_t* cb, int fs) {
+		unsigned int ap;
+		unsigned int h;
+		// approximate (A * p) with "partial rounding"
+		ap = ((A >> PBITS) | ((A >> (PBITS - 1)) & 1))* p;
+		h = A - ap;
+		if (C >= h) {
+			*b = 0;
+			C -= h;
+			A = ap;
+		}
+		else {
+			*b = 1;
+			A = h;
+		}
+		while (A < HALF) {
+			A <<= 1;
+			// Use new flushing technique; insert zero in LSB of C if reading past the end of the arithmetic code
+			C <<= 1;
+			if (cbptr < fs) {
+				C |= GET_BIT(cb, cbptr);
+			}
+			cbptr++;
+		}
+	}
+
+	void decodeBit_Flush(uint8_t* b, int p, uint8_t* cb, int fs) {
+		(void)p;
+		Init = 1;
+		if (cbptr < fs - 7) {
+			*b = 0;
+		}
+		else {
+			*b = 1;
+			while ((cbptr < fs) && (*b == 1)) {
+				if (GET_BIT(cb, cbptr) != 0) {
+					*b = 1;
+				}
+				cbptr++;
+			}
+		}
+	}
+
+};
+
+}
+
+#endif

+ 132 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/binding/dst_decoder_mt.cpp

@@ -0,0 +1,132 @@
+/*
+* SACD Decoder plugin
+* Copyright (c) 2011-2020 Maxim V.Anisiutkin <maxim.anisiutkin@gmail.com>
+*
+* This program is free software; you can redistribute it and/or
+* modify it under the terms of the GNU Lesser General Public
+* License as published by the Free Software Foundation; either
+* version 2.1 of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public
+* License along with FFmpeg; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <memory.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include "dst_decoder_mt.h"
+
+#define DSD_SILENCE_BYTE 0x69
+
+static void dst_run_thread(frame_slot_t& slot) {
+	while (slot.run_slot) {
+		slot.dst_semaphore.wait();
+		if (slot.run_slot) {
+			slot.state = slot_state_t::SLOT_RUNNING;
+			const int rc = slot.dec.decode(slot.dst_data, slot.dst_size * 8, slot.dsd_data);
+			slot.state = rc == 0 ? slot_state_t::SLOT_READY : slot_state_t::SLOT_READY_WITH_ERROR;
+		}
+		else {
+			slot.dsd_data = nullptr;
+			slot.dst_size = 0;
+		}
+		slot.dsd_semaphore.notify();
+	}
+}
+
+dst_decoder_t::dst_decoder_t(unsigned int threads) {
+	frame_slots.resize(threads);
+	slot_nr            = 0;
+	channel_count      = 0;
+	channel_frame_size = 0;
+}
+
+dst_decoder_t::~dst_decoder_t() {
+	for (auto& slot : frame_slots) {
+		slot.state = slot_state_t::SLOT_TERMINATING;
+		slot.run_slot = false;
+		if (slot.run_thread.joinable()) {
+			slot.dst_semaphore.notify(); // Release worker (decoding) thread for exit
+			slot.run_thread.join(); // Wait until worker (decoding) thread exit
+		}
+		slot.dec.close();
+	}
+}
+
+unsigned int dst_decoder_t::get_slot_nr() {
+	return slot_nr;
+}
+
+int dst_decoder_t::init(unsigned int channels, unsigned int samplerate, unsigned int framerate) {
+	channel_count = channels;
+	channel_frame_size = samplerate / 8 / framerate;
+	for (auto& slot : frame_slots) {
+		if (slot.dec.init(channel_count, channel_frame_size) == 0) {
+			slot.channel_count = channel_count;
+			slot.channel_frame_size = channel_frame_size;
+			slot.dsd_size = (size_t)(channel_count * channel_frame_size);
+			slot.run_slot = true;
+			slot.run_thread = thread(dst_run_thread, ref(slot));
+			if (!slot.run_thread.joinable()) {
+				kodiLog(ADDON_LOG_ERROR, ("Could not start decoder thread"));
+				return -1;
+			}
+		}
+		else {
+			kodiLog(ADDON_LOG_ERROR, ("Could not initialize decoder slot"));
+			return -1;
+		}
+	}
+	return 0;
+}
+
+int dst_decoder_t::decode(uint8_t* dst_data, size_t dst_size, uint8_t** dsd_data, size_t* dsd_size) {
+
+	/* Get current slot */
+	frame_slot_t& slot_set = frame_slots[slot_nr];
+
+	/* Allocate encoded frame into the slot */
+	slot_set.dsd_data = *dsd_data;
+	slot_set.dst_data = dst_data;
+	slot_set.dst_size = dst_size;
+
+	/* Release worker (decoding) thread on the loaded slot */
+	if (dst_size > 0) {
+		slot_set.state = slot_state_t::SLOT_LOADED;
+		slot_set.dst_semaphore.notify();
+	}
+	else {
+		slot_set.state = slot_state_t::SLOT_EMPTY;
+	}
+
+	/* Move to the oldest slot */
+	slot_nr = (slot_nr + 1) % frame_slots.size();
+	frame_slot_t& slot_get = frame_slots[slot_nr];
+
+	/* Dump decoded frame */
+	if (slot_get.state != slot_state_t::SLOT_EMPTY) {
+		slot_get.dsd_semaphore.wait();
+	}
+	switch (slot_get.state) {
+	case slot_state_t::SLOT_READY:
+		*dsd_data = slot_get.dsd_data;
+		*dsd_size = (size_t)(channel_count * channel_frame_size);
+		return 0;
+	case slot_state_t::SLOT_READY_WITH_ERROR:
+		*dsd_data = slot_get.dsd_data;
+		*dsd_size = (size_t)(channel_count * channel_frame_size);
+		memset(*dsd_data, DSD_SILENCE_BYTE, *dsd_size);
+		return -1;
+	default:
+		*dsd_data = nullptr;
+		*dsd_size = 0;
+		return -1;
+	}
+}

+ 87 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/binding/dst_decoder_mt.h

@@ -0,0 +1,87 @@
+/*
+* SACD Decoder plugin
+* Copyright (c) 2011-2020 Maxim V.Anisiutkin <maxim.anisiutkin@gmail.com>
+*
+* This program is free software; you can redistribute it and/or
+* modify it under the terms of the GNU Lesser General Public
+* License as published by the Free Software Foundation; either
+* version 2.1 of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public
+* License along with FFmpeg; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#ifndef _DST_DECODER_MT_H_INCLUDED
+#define _DST_DECODER_MT_H_INCLUDED
+
+#include <thread>
+#include <vector>
+#include "semaphore.h"
+#include "decoder.h"
+
+using std::thread;
+using std::vector;
+using std::ref;
+using dst::decoder_t;
+
+enum class slot_state_t {SLOT_EMPTY, SLOT_LOADED, SLOT_RUNNING, SLOT_READY, SLOT_READY_WITH_ERROR, SLOT_TERMINATING};
+
+class frame_slot_t {
+public:
+	bool         run_slot;
+	thread       run_thread;
+	semaphore    dsd_semaphore;
+	semaphore    dst_semaphore;
+
+	slot_state_t state;
+	uint8_t*     dsd_data;
+	unsigned int dsd_size;
+	uint8_t*     dst_data;
+	unsigned int dst_size;
+	unsigned int channel_count;
+	unsigned int channel_frame_size;
+	decoder_t    dec;
+
+	frame_slot_t() {
+		run_slot = false;
+		state = slot_state_t::SLOT_EMPTY;
+		dsd_data = nullptr;
+		dsd_size = 0;
+		dst_data = nullptr;
+		dst_size = 0;
+		channel_count = 0;
+		channel_frame_size = 0;
+	}
+	frame_slot_t(const frame_slot_t& slot) {
+		run_slot = slot.run_slot;
+		state = slot.state;
+		dsd_data = slot.dsd_data;
+		dsd_size = slot.dsd_size;
+		dst_data = slot.dst_data;
+		dst_size = slot.dst_size;
+		channel_count = slot.channel_count;
+		channel_frame_size = slot.channel_frame_size;
+	}
+	frame_slot_t& operator=(const frame_slot_t& slot) = delete;
+};
+
+class dst_decoder_t {
+	vector<frame_slot_t> frame_slots;
+	unsigned int slot_nr;       
+	unsigned int channel_count;
+	unsigned int channel_frame_size;
+public:
+	dst_decoder_t(unsigned int threads);
+	~dst_decoder_t();
+	unsigned int get_slot_nr();
+	int init(unsigned int channels, unsigned int samplerate, unsigned int framerate);
+	int decode(uint8_t* dst_data, size_t dst_size, uint8_t** dsd_data, size_t* dsd_size);
+};
+
+#endif

+ 43 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/binding/semaphore.h

@@ -0,0 +1,43 @@
+#ifndef _SEMAPHORE_H_INCLUDED
+#define _SEMAPHORE_H_INCLUDED
+
+#include <mutex>
+#include <condition_variable>
+
+using std::mutex;
+using std::condition_variable;
+using std::lock_guard;
+using std::unique_lock;
+
+class semaphore {
+	mutex m_mtx;
+	condition_variable m_cv;
+	int m_cnt = 0;
+public:
+	semaphore() = default;
+	semaphore(const semaphore& sem) = delete;
+	semaphore(semaphore&& sem) = delete;
+	semaphore operator=(const semaphore& sem) = delete;
+	void notify() {
+		lock_guard<mutex> lock(m_mtx);
+		m_cnt++;
+		m_cv.notify_one();
+	}
+	void wait() {
+		unique_lock<mutex> lock(m_mtx);
+		while (!m_cnt) {
+			m_cv.wait(lock);
+		}
+		m_cnt--;
+	}
+	bool try_wait() {
+		lock_guard<mutex> lock(m_mtx);
+		if (m_cnt) {
+			m_cnt--;
+			return true;
+		}
+		return false;
+	}
+};
+
+#endif

+ 95 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/common.h

@@ -0,0 +1,95 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef COMMON_H
+#define COMMON_H
+
+#include <cstdarg>
+#include <cstdio>
+#include <stdint.h>
+#include "hilog/log.h"
+
+enum ADDON_LOG
+{
+  ADDON_LOG_DEBUG = 0,
+  ADDON_LOG_INFO = 1,
+  ADDON_LOG_WARNING = 2,
+  ADDON_LOG_ERROR = 3,
+  ADDON_LOG_FATAL = 4,
+};
+
+namespace
+{
+
+constexpr unsigned int DSTDEC_LOG_DOMAIN = 0xD001780;
+constexpr const char* DSTDEC_LOG_TAG = "dstdec";
+
+inline const char* kodiTranslateLogLevel(const ADDON_LOG logLevel)
+{
+  switch (logLevel)
+  {
+    case ADDON_LOG_DEBUG:
+      return "LOG_DEBUG:   ";
+    case ADDON_LOG_INFO:
+      return "LOG_INFO:    ";
+    case ADDON_LOG_WARNING:
+      return "LOG_WARNING: ";
+    case ADDON_LOG_ERROR:
+      return "LOG_ERROR:   ";
+    case ADDON_LOG_FATAL:
+      return "LOG_FATAL:   ";
+    default:
+      break;
+  }
+  return "LOG_UNKNOWN: ";
+}
+
+inline LogLevel toHilogLevel(const ADDON_LOG logLevel)
+{
+  switch (logLevel)
+  {
+    case ADDON_LOG_DEBUG:
+      return LOG_DEBUG;
+    case ADDON_LOG_INFO:
+      return LOG_INFO;
+    case ADDON_LOG_WARNING:
+      return LOG_WARN;
+    case ADDON_LOG_ERROR:
+    case ADDON_LOG_FATAL:
+      return LOG_ERROR;
+    default:
+      return LOG_INFO;
+  }
+}
+
+inline void kodiLog(const ADDON_LOG logLevel, const char* format, ...)
+{
+  char buffer[16384];
+  va_list args;
+  va_start(args, format);
+  vsnprintf(buffer, sizeof(buffer), format, args);
+  va_end(args);
+
+  OH_LOG_Print(LOG_APP, toHilogLevel(logLevel), DSTDEC_LOG_DOMAIN, DSTDEC_LOG_TAG, "%{public}s", buffer);
+#ifdef DEBUG
+  fprintf(stderr, "%s%s\n", kodiTranslateLogLevel(logLevel), buffer);
+#endif
+}
+
+}
+
+namespace dst {
+
+static auto GET_BIT = [](auto base, auto index) {
+	return (((unsigned char*)base)[index >> 3] >> (7 - (index & 7))) & 1;
+};
+
+static auto GET_NIBBLE = [](auto base, auto index) {
+	return (((unsigned char*)base)[index >> 1] >> ((index & 1) << 2)) & 0x0f;
+};
+
+}
+
+#endif

+ 47 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/consts.h

@@ -0,0 +1,47 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef CONSTS_H
+#define CONSTS_H
+
+namespace dst {
+
+// Prediction
+constexpr int SIZE_CODEDPREDORDER = 7; // Number of bits in the stream for representing	the CodedPredOrder in each frame
+constexpr int MAXPREDORDER = 1 << SIZE_CODEDPREDORDER; // Maximum prediction filter order
+
+// Probability tables
+constexpr int SIZE_CODEDPTABLELEN = 6; // Number bits for p-table length
+constexpr int MAXPTABLELEN = 1 << SIZE_CODEDPTABLELEN; // Maximum length of p-tables
+
+constexpr int SIZE_PREDCOEF = 9; // Number of bits in the stream for representing each filter coefficient in each frame
+
+// Arithmetic coding
+constexpr int AC_BITS    = 8; // Number of bits and maximum level for coding the probability
+constexpr int AC_PROBS   = 1 << AC_BITS;
+constexpr int AC_HISBITS = 6; // Number of entries in the histogram
+constexpr int AC_HISMAX  = 1 << AC_HISBITS;
+constexpr int AC_QSTEP   = SIZE_PREDCOEF - AC_HISBITS; // Quantization step for histogram
+
+// Rice coding of filter coefficients and probability tables
+constexpr int NROFFRICEMETHODS = 3; // Number of different Pred. Methods for filters	used in combination with Rice coding
+constexpr int NROFPRICEMETHODS = 3; // Number of different Pred. Methods for Ptables	used in combination with Rice coding
+constexpr int MAXCPREDORDER    = 3; // max pred.order for prediction of filter coefs / Ptables entries
+constexpr int SIZE_RICEMETHOD  = 2; // nr of bits in stream for indicating method
+constexpr int SIZE_RICEM       = 3; // nr of bits in stream for indicating m
+constexpr int MAX_RICE_M_F     = 6; // Max. value of m for filters
+constexpr int MAX_RICE_M_P     = 4; // Max. value of m for Ptables
+
+// Segmentation
+constexpr int MAXNROF_FSEGS = 4;   // max nr of segments per channel for filters
+constexpr int MAXNROF_PSEGS = 8;   // max nr of segments per channel for Ptables
+constexpr int MIN_FSEG_LEN = 1024; // min segment length in bits of filters
+constexpr int MIN_PSEG_LEN = 32;   // min segment length in bits of Ptables
+
+constexpr int MAXNROF_SEGS = 8; // max nr of segments per channel for filters or Ptables
+
+}
+
+#endif

+ 84 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/ct.h

@@ -0,0 +1,84 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef CT_H
+#define CT_H
+
+#include <stdint.h>
+#include <array>
+#include <vector>
+#include "consts.h"
+
+using std::array;
+using std::vector;
+
+namespace dst
+{
+
+enum class ct_e { Filter, Ptable };
+
+constexpr int MAXFILTERBITS = MAXPREDORDER * SIZE_PREDCOEF;
+constexpr int MAXPTABLEBITS = AC_HISMAX * AC_BITS;
+
+template<ct_e ct_type>
+class ct_t {
+protected:
+	static constexpr int ct_size = (ct_type == ct_e::Filter) ? MAXPREDORDER : AC_HISMAX;
+	static constexpr int ct_bits = (ct_type == ct_e::Filter) ? MAXFILTERBITS : MAXPTABLEBITS;
+public:
+	unsigned int NrOfTables;                         // Number of coded tables
+	unsigned int StreamBits;                         // Number of bits all filters use in the stream
+	unsigned int CPredOrder[NROFFRICEMETHODS];       // Code_PredOrder[Method]
+	int CPredCoef[NROFPRICEMETHODS][MAXCPREDORDER];  // Code_PredCoef[Method][CoefNr]
+	vector<bool> Coded;                              // DST encode coefs/entries of Fir/PtabNr
+	vector<unsigned int> BestMethod;                 // BestMethod[Fir/PtabNr]
+	vector<array<unsigned int, NROFFRICEMETHODS>> m; // m[Fir/PtabNr][Method]
+	vector<unsigned int> DataLenData;                // Fir/PtabDataLength[Fir/PtabNr]
+	vector<array<int, ct_size>> data;                // Fir/PtabData[FirNr][Index]
+public:
+	ct_t() {
+		if constexpr(ct_type == ct_e::Filter) {
+			CPredOrder[0] = 1;
+			CPredCoef[0][0] = -8;
+			CPredOrder[1] = 2;
+			CPredCoef[1][0] = -16;
+			CPredCoef[1][1] = 8;
+			CPredOrder[2] = 3;
+			CPredCoef[2][0] = -9;
+			CPredCoef[2][1] = -5;
+			CPredCoef[2][2] = 6;
+			if constexpr (NROFFRICEMETHODS == 4) {
+				CPredOrder[3] = 1;
+				CPredCoef[3][0] = 8;
+			}
+		}
+		if constexpr(ct_type == ct_e::Ptable) {
+			CPredOrder[0] = 1;
+			CPredCoef[0][0] = -8;
+			CPredOrder[1] = 2;
+			CPredCoef[1][0] = -16;
+			CPredCoef[1][1] = 8;
+			CPredOrder[2] = 3;
+			CPredCoef[2][0] = -24;
+			CPredCoef[2][1] = 24;
+			CPredCoef[2][2] = -8;
+		}
+	}
+	void init(unsigned int tables) {
+		NrOfTables = tables;
+		Coded.resize(tables);
+		BestMethod.resize(tables);
+		m.resize(tables);
+		DataLenData.resize(tables);
+		data.resize(tables);
+	}
+};
+
+typedef ct_t<ct_e::Filter> ft_t;
+typedef ct_t<ct_e::Ptable> pt_t;
+
+}
+
+#endif

+ 291 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/decoder/decoder.cpp

@@ -0,0 +1,291 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#include <memory.h>
+#include "decoder.h"
+
+namespace dst
+{
+
+static unsigned int int_abs(int n) {
+	int mask = n >> (8 * sizeof(int) - 1);
+	return (unsigned int)((n + mask) ^ mask);
+}
+
+static unsigned int int_log2(unsigned int n) {
+	return (n > 1) ? 1 + int_log2(n >> 1) : 0;
+}
+
+int          decoder_t::GC_ICoefSign[256];
+unsigned int decoder_t::GC_ICoefIndex[256];
+bool         decoder_t::GC_ICoefInit = false;
+
+decoder_t::decoder_t() {
+	if (!GC_ICoefInit) {
+		GC_ICoefSign[0] = 0;
+		GC_ICoefIndex[0] = (unsigned int)-1;
+		for (int i = 1; i < 256; i++) {
+			auto i_gray_delta = (i ^ (i >> 1)) - ((i - 1) ^ ((i - 1) >> 1));
+			auto i_gray_delta_abs = int_abs(i_gray_delta);
+			auto i_gray_index = int_log2(i_gray_delta_abs);
+			if (i_gray_delta > 0) {
+				GC_ICoefSign[i] = +1;
+			}
+			if (i_gray_delta < 0) {
+				GC_ICoefSign[i] = -1;
+			}
+			GC_ICoefIndex[i] = i_gray_index;
+		}
+		GC_ICoefInit = true;
+	}
+}
+
+decoder_t::~decoder_t() {
+}
+
+int decoder_t::init(unsigned int channels, unsigned int channel_frame_size) {
+	m_fr.init(channels, channel_frame_size);
+	m_ft.init(2 * channels);
+	m_pt.init(2 * channels);
+	P_one.resize(2 * channels);
+	AData.resize(channels * channel_frame_size);
+	LT_ICoefI.resize(2 * channels);
+	LT_Status.resize(channels);
+	return 0;
+}
+
+int decoder_t::close() {
+	return 0;
+}
+
+// Decode a complete frame (all channels)
+int decoder_t::decode(const uint8_t* dst_data, unsigned int dst_bits, uint8_t* dsd_data) {
+	int     rv = 0;
+	uint8_t ACError;
+
+	auto NrOfBitsPerCh = m_fr.NrOfBitsPerCh;
+	auto NrOfChannels = m_fr.NrOfChannels;
+
+	m_fr.CalcNrOfBytes = dst_bits / 8;
+	m_fr.CalcNrOfBits = m_fr.CalcNrOfBytes * 8;
+
+	/* unpack DST frame: segmentation, mapping, arithmetic data */
+	rv = unpack(dst_data, dsd_data);
+	if (rv == -1) {
+		return -1;
+	}
+
+	if (m_fr.DSTCoded == 1) {
+		ac_t AC;
+
+		fillTable4Bit(m_fr.FSegment, m_fr.Filter4Bit);
+		fillTable4Bit(m_fr.PSegment, m_fr.Ptable4Bit);
+
+		GC_InitCoefTables(LT_ICoefI);
+		LT_InitStatus(LT_Status);
+
+		AC.decodeBit_Init(AData.data(), ADataLen);
+		AC.decodeBit_Decode(&ACError, reverse7LSBs(m_fr.ICoefA[0][0]), AData.data(), ADataLen);
+
+		memset(dsd_data, 0, (NrOfBitsPerCh * NrOfChannels + 7) / 8);
+		for (auto BitNr = 0u; BitNr < NrOfBitsPerCh; BitNr++) {
+			for (auto ChNr = 0u; ChNr < NrOfChannels; ChNr++) {
+				int16_t Predict;
+				uint8_t Residual;
+				int16_t BitVal;
+				auto FilterNr = GET_NIBBLE(m_fr.Filter4Bit[ChNr].data(), BitNr);
+
+				/* Calculate output value of the FIR filter */
+				Predict = LT_RunFilter(LT_ICoefI[FilterNr], LT_Status[ChNr]);
+
+				/* Arithmetic decode the incoming bit */
+				if ((m_fr.HalfProb[ChNr]/* == 1*/) && (BitNr < m_fr.NrOfHalfBits[ChNr])) {
+					AC.decodeBit_Decode(&Residual, AC_PROBS / 2, AData.data(), ADataLen);
+				}
+				else {
+					auto PtableNr = GET_NIBBLE(m_fr.Ptable4Bit[ChNr].data(), BitNr);
+					auto PtableIndex = AC.getPtableIndex(Predict, m_fr.PtableLen[PtableNr]);
+					AC.decodeBit_Decode(&Residual, P_one[PtableNr][PtableIndex], AData.data(), ADataLen);
+				}
+
+				/* Channel bit depends on the predicted bit and BitResidual[][] */
+				BitVal = (int16_t)(((((uint16_t)Predict) >> 15) ^ Residual) & 1);
+
+				/* Shift the result into the correct bit position */
+				dsd_data[(BitNr >> 3) * NrOfChannels + ChNr] |= (uint8_t)(BitVal << (7 - (BitNr & 7)));
+
+				/* Update filter */
+				{
+					uint64_t* const st = reinterpret_cast<uint64_t*>(LT_Status[ChNr].data());
+					st[1] = (st[1] << 1) | (st[0] >> 63);
+					st[0] = (st[0] << 1) | BitVal;
+				}
+			}
+		}
+
+		/* Flush the arithmetic decoder */
+		AC.decodeBit_Flush(&ACError, 0, AData.data(), ADataLen);
+
+		if (ACError != 1) {
+			kodiLog(ADDON_LOG_ERROR, "Arithmetic decoding error");
+			rv = -1;
+		}
+	}
+
+	return rv;
+}
+
+// Read a complete frame from the DST input stream
+int decoder_t::unpack(const uint8_t* dst_data, uint8_t* dsd_data) {
+	m_fr.set_data(dst_data, m_fr.CalcNrOfBytes); // Assign DST data from input stream
+	m_fr.DSTCoded = m_fr.get_bit(); // Read Processing_Mode (Table 10.4)
+	if (!m_fr.DSTCoded) {
+		int unused;
+		unused = m_fr.get_uint(1); // Read DST_X_Bit (Table 10.4)
+		unused = m_fr.get_uint(6); // Read Reserved (Table 10.4)
+		if (unused) {
+			kodiLog(ADDON_LOG_ERROR, "Illegal stuffing pattern in frame");
+			return -1;
+		}
+		m_fr.read_dsd_data(dsd_data); // Read DSD data and put in output stream
+	}
+	else {
+		m_fr.read_segmentation(); // Read Segmentation (Table 10.4)
+		m_fr.read_mapping(); // Read Mapping (Table 10.4)
+		m_fr.read_filter_coef_sets(m_ft); // Read Filter_Coef_Sets (Table 10.4)
+		m_fr.read_probability_tables(m_pt, P_one); // Read Probability_Tables (Table 10.4)
+		ADataLen = m_fr.CalcNrOfBits - m_fr.get_offset();
+		m_fr.read_arithmetic_coded_data(AData.data()); // Read Arithmetic_Coded_Data (Table 10.4)
+		if (ADataLen > 0 && GET_BIT(AData.data(), 0) != 0) {
+			kodiLog(ADDON_LOG_ERROR, "Illegal arithmetic code in frame");
+			return -1;
+		}
+	}
+	return 0;
+}
+
+/* Take the 7 LSBs of a number consisting of SIZE_PREDCOEF bits */
+/* (2's complement), reverse the bit order and add 1 to it.     */
+
+int16_t decoder_t::reverse7LSBs(int16_t c) {
+	const int16_t reverse[128] = {
+		1, 65, 33, 97, 17, 81, 49, 113, 9, 73, 41, 105, 25, 89, 57, 121,
+		5, 69, 37, 101, 21, 85, 53, 117, 13, 77, 45, 109, 29, 93, 61, 125,
+		3, 67, 35, 99, 19, 83, 51, 115, 11, 75, 43, 107, 27, 91, 59, 123,
+		7, 71, 39, 103, 23, 87, 55, 119, 15, 79, 47, 111, 31, 95, 63, 127,
+		2, 66, 34, 98, 18, 82, 50, 114, 10, 74, 42, 106, 26, 90, 58, 122,
+		6, 70, 38, 102, 22, 86, 54, 118, 14, 78, 46, 110, 30, 94, 62, 126,
+		4, 68, 36, 100, 20, 84, 52, 116, 12, 76, 44, 108, 28, 92, 60, 124,
+		8, 72, 40, 104, 24, 88, 56, 120, 16, 80, 48, 112, 32, 96, 64, 128 };
+	return reverse[(c + (1 << SIZE_PREDCOEF)) & 127];
+}
+
+/* Fill an array that indicates for each bit of each channel which table number must be used */
+
+void decoder_t::fillTable4Bit(segment_t& S, vector<vector<uint8_t>>& Table4Bit) {
+	unsigned int SegNr;
+	unsigned int Start;
+	unsigned int End;
+	int8_t Val;
+	for (auto ChNr = 0u; ChNr < m_fr.NrOfChannels; ChNr++) {
+		for (SegNr = 0u, Start = 0; SegNr < S.NrOfSegments[ChNr] - 1; SegNr++) {
+			Val = (int8_t)S.Table4Segment[ChNr][SegNr];
+			End = Start + S.Resolution * 8 * S.SegmentLength[ChNr][SegNr];
+			for (auto BitNr = Start; BitNr < End; BitNr++) {
+				uint8_t* p = (uint8_t*)&Table4Bit[ChNr][BitNr / 2];
+				auto s = (BitNr & 1) << 2;
+				*p = (uint8_t)(((uint8_t)Val << s) | (*p & (0xf0 >> s)));
+			}
+			Start += S.Resolution * 8 * S.SegmentLength[ChNr][SegNr];
+		}
+		Val = (int8_t)S.Table4Segment[ChNr][SegNr];
+		for (auto BitNr = Start; BitNr < m_fr.NrOfBitsPerCh; BitNr++) {
+			uint8_t* p = (uint8_t*)&Table4Bit[ChNr][BitNr / 2];
+			auto s = (BitNr & 1) << 2;
+			*p = (uint8_t)(((uint8_t)Val << s) | (*p & (0xf0 >> s)));
+		}
+	}
+}
+
+void decoder_t::LT_InitCoefTables(vector<array<array<int16_t, 256>, 16>>& ICoefI) {
+	for (auto FilterNr = 0u; FilterNr < m_fr.NrOfFilters; FilterNr++) {
+		auto FilterLength = m_fr.PredOrder[FilterNr];
+		for (auto TableNr = 0u; TableNr < 16u; TableNr++) {
+			auto k = (int)FilterLength - (int)TableNr * 8;
+			if (k > 8) {
+				k = 8;
+			}
+			else if (k < 0) {
+				k = 0;
+			}
+			for (auto i = 0; i < 256; i++) {
+				int cvalue = 0;
+				for (auto j = 0; j < k; j++) {
+					cvalue += (((i >> j) & 1) * 2 - 1) * m_fr.ICoefA[FilterNr][TableNr * 8 + j];
+				}
+				ICoefI[FilterNr][TableNr][i] = (int16_t)cvalue;
+			}
+		}
+	}
+}
+
+void decoder_t::GC_InitCoefTables(vector<array<array<int16_t, 256>, 16>>& ICoefI) {
+	for (auto FilterNr = 0u; FilterNr < m_fr.NrOfFilters; FilterNr++) {
+		auto FilterLength = m_fr.PredOrder[FilterNr];
+		for (auto TableNr = 0u; TableNr < 16u; TableNr++) {
+			auto k = (int)FilterLength - (int)TableNr * 8;
+			if (k > 8) {
+				k = 8;
+			}
+			else if (k < 0) {
+				k = 0;
+			}
+			int cvalue = 0;
+			for (auto j = 0; j < k; j++) {
+				cvalue -= m_fr.ICoefA[FilterNr][TableNr * 8 + j];
+			}
+			ICoefI[FilterNr][TableNr][0] = (int16_t)cvalue;
+			for (auto i = 1; i < 256; i++) {
+				auto i_gray = i ^ (i >> 1);
+				auto j_gray = GC_ICoefIndex[i];
+				if (j_gray < (unsigned int)k) {
+					cvalue += GC_ICoefSign[i] * (m_fr.ICoefA[FilterNr][TableNr * 8 + j_gray] << 1);
+				}
+				ICoefI[FilterNr][TableNr][i_gray] = (int16_t)cvalue;
+			}
+		}
+	}
+}
+
+void decoder_t::LT_InitStatus(vector<array<uint8_t, 16>>& Status) {
+	for (auto ChNr = 0u; ChNr < m_fr.NrOfChannels; ChNr++) {
+		for (auto TableNr = 0u; TableNr < 16u; TableNr++) {
+			Status[ChNr][TableNr] = 0xaa;
+		}
+	}
+}
+
+int16_t decoder_t::LT_RunFilter(array<array<int16_t, 256>, 16>& FilterTable, array<uint8_t, 16>& ChannelStatus) {
+	int Predict;
+	Predict = FilterTable[0][ChannelStatus[0]];
+	Predict += FilterTable[1][ChannelStatus[1]];
+	Predict += FilterTable[2][ChannelStatus[2]];
+	Predict += FilterTable[3][ChannelStatus[3]];
+	Predict += FilterTable[4][ChannelStatus[4]];
+	Predict += FilterTable[5][ChannelStatus[5]];
+	Predict += FilterTable[6][ChannelStatus[6]];
+	Predict += FilterTable[7][ChannelStatus[7]];
+	Predict += FilterTable[8][ChannelStatus[8]];
+	Predict += FilterTable[9][ChannelStatus[9]];
+	Predict += FilterTable[10][ChannelStatus[10]];
+	Predict += FilterTable[11][ChannelStatus[11]];
+	Predict += FilterTable[12][ChannelStatus[12]];
+	Predict += FilterTable[13][ChannelStatus[13]];
+	Predict += FilterTable[14][ChannelStatus[14]];
+	Predict += FilterTable[15][ChannelStatus[15]];
+	return (int16_t)Predict;
+}
+
+}

+ 56 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/decoder/decoder.h

@@ -0,0 +1,56 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef DECODER_H
+#define DECODER_H
+
+#include <stdint.h>
+#include <array>
+#include <vector>
+#include "segment.h"
+#include "ct.h"
+#include "fh.h"
+#include "ac.h"
+#include "fr.h"
+#include "stream.h"
+
+using std::array;
+using std::vector;
+
+namespace dst
+{
+
+class decoder_t {
+	static int          GC_ICoefSign[256];
+	static unsigned int GC_ICoefIndex[256];
+	static bool         GC_ICoefInit;
+public:
+	fr_t m_fr;                                    // Contains frame based header information
+	ft_t m_ft;                                    // Contains FIR-coef. compression data
+	pt_t m_pt;                                    // Contains Ptable-entry compression data
+	vector<array<unsigned int, AC_HISMAX>> P_one; // Probability table for arithmetic coder
+	vector<uint8_t> AData;                        // Contains the arithmetic coded bit stream of a complete frame	vector<uint8_t> AData;               // Contains the arithmetic coded bit stream of a complete frame
+	int             ADataLen;                     // Number of code bits contained in AData[]
+	vector<array<array<int16_t, 256>, 16>> LT_ICoefI;
+	vector<array<uint8_t, 16>>             LT_Status;
+public:
+	decoder_t();
+	~decoder_t();
+	int init(unsigned int channels, unsigned int channel_frame_size);
+	int close();
+	int decode(const uint8_t* dst_data, unsigned int dst_bits, uint8_t* dsd_data);
+private:
+	int unpack(const uint8_t* dst_data, uint8_t* dsd_data);
+	int16_t reverse7LSBs(int16_t c);
+	void fillTable4Bit(segment_t& S, vector<vector<uint8_t>>& Table4Bit);
+	void LT_InitCoefTables(vector<array<array<int16_t, 256>, 16>>& ICoefI);
+	void GC_InitCoefTables(vector<array<array<int16_t, 256>, 16>>& ICoefI);
+	void LT_InitStatus(vector<array<uint8_t, 16>>& Status);
+	int16_t LT_RunFilter(array<array<int16_t, 256>, 16>& FilterTable, array<uint8_t, 16>& ChannelStatus);
+};
+
+}
+
+#endif

+ 78 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/fh.h

@@ -0,0 +1,78 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef FH_H
+#define FH_H
+
+#include <stdint.h>
+#include <array>
+#include <vector>
+#include "consts.h"
+#include "segment.h"
+
+using std::array;
+using std::vector;
+
+namespace dst
+{
+
+class fh_t {
+public:
+	unsigned int       NrOfChannels;                                       // Number of channels in the recording
+	unsigned int       NrOfFilters;                                        // Number of filters used for this frame
+	unsigned int       NrOfPtables;                                        // Number of Ptables used for this frame
+	vector<unsigned int> PredOrder;                                        // Prediction order used for this frame
+	vector<unsigned int> PtableLen;                                        // Nr of Ptable entries used for this frame
+	vector<array<int16_t, 1 << SIZE_CODEDPREDORDER>> ICoefA;      // Integer coefs for actual coding
+	unsigned int       CalcNrOfBytes;                                      // Contains number of bytes of the complete channel stream after arithmetic encoding (also containing bytestuff-, ICoefA-bits, etc.)
+	unsigned int       CalcNrOfBits;                                       // Contains number of bits of the complete channel stream after arithmetic encoding (also containing bytestuff-, ICoefA-bits, etc.)
+	vector<unsigned int> HalfProb;                                         // Defines per channel which probability is applied for the first PredOrder[] bits of a frame (0 = use Ptable entry, 1 = 128)
+	vector<unsigned int> NrOfHalfBits;                                     // Defines per channel how many bits at the start of each frame are optionally coded with p=0.5
+	segment_t FSegment;                                           // Contains segmentation data for filters
+	vector<vector<uint8_t>> Filter4Bit;                           // Filter4Bit[ChNr][BitNr]
+	segment_t PSegment;                                           // Contains segmentation data for Ptables
+	vector<vector<uint8_t>> Ptable4Bit;                           // Ptable4Bit[ChNr][BitNr]
+	bool      DSTCoded;                                           // true if DST coded is put in DST stream, false if DSD is put in DST stream
+	bool      PSameSegAsF;                                        // true if segmentation is equal for F and P
+	bool      PSameMapAsF;                                        // true if mapping is equal for F and P
+	bool      FSameSegAllCh;                                      // true if all channels have same Filtersegm
+	bool      FSameMapAllCh;                                      // true if all channels have same Filtermap
+	bool      PSameSegAllCh;                                      // true if all channels have same Ptablesegm
+	bool      PSameMapAllCh;                                      // true if all channels have same Ptablemap
+	bool      unused;
+	unsigned int       SegAndMapBits;                                      // Number of bits in the stream for Seg&Map
+	unsigned int       MaxNrOfFilters;                                     // Max. nr. of filters allowed per frame
+	unsigned int       MaxNrOfPtables;                                     // Max. nr. of Ptables allowed per frame
+	unsigned int       MaxFrameLen;                                        // Max frame length of stream
+	unsigned int       ByteStreamLen;                                      // MaxFrameLen * NrOfChannels
+	unsigned int       BitStreamLen;                                       // ByteStreamLen * RESOL
+	unsigned int       NrOfBitsPerCh;                                      // MaxFrameLen * RESOL
+public:
+	void init(unsigned int channels, unsigned int channel_frame_size) {
+		NrOfChannels = channels;
+		MaxFrameLen = channel_frame_size;
+		ByteStreamLen = MaxFrameLen * NrOfChannels;
+		BitStreamLen = ByteStreamLen * 8;
+		NrOfBitsPerCh = MaxFrameLen * 8;
+		MaxNrOfFilters = 2 * NrOfChannels;
+		MaxNrOfPtables = 2 * NrOfChannels;
+		PredOrder.resize(MaxNrOfFilters);
+		PtableLen.resize(MaxNrOfPtables);
+		ICoefA.resize(MaxNrOfFilters);
+		HalfProb.resize(channels);
+		NrOfHalfBits.resize(channels);
+		Filter4Bit.resize(channels);
+		for (auto& t : Filter4Bit) t.resize(4 * channel_frame_size);
+		Ptable4Bit.resize(channels);
+		for (auto& t : Ptable4Bit) t.resize(4 * channel_frame_size);
+		FSegment.init(channels);
+		PSegment.init(channels);
+	}
+};
+
+}
+
+#endif
+

+ 419 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/fr.h

@@ -0,0 +1,419 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef FR_H
+#define FR_H
+
+#include <stdint.h>
+#include "consts.h"
+#include "fh.h"
+#include "ct.h"
+#include "stream.h"
+
+namespace dst
+{
+
+class fr_t : public stream_t, public fh_t {
+public:
+
+	// Calculate the log2 of an integer and round the result up by using integer arithmetic
+	unsigned int log2_round_up(unsigned int x) {
+		unsigned int y = 0;
+		while (x >= (1u << y)) {
+			y++;
+		}
+		return y;
+	}
+
+	// Rice decoding of encoded Filters and Ptables
+	int rice_decode(unsigned int m) {
+		int LSBs;
+		int Nr;
+		int RLBit;
+		int RunLength;
+		int Sign;
+
+		// Retrieve run length code
+		RunLength = 0;
+		do {
+			RLBit = get_bit(); // Read RL_Bit (Table 10.13)
+			RunLength += (1 - RLBit);
+		} while (!RLBit);
+		// Retrieve least significant bits
+		LSBs = (int)get_uint(m); // Read LSBs (Table 10.13)
+		Nr = (RunLength << m) + LSBs;
+		/* Retrieve optional sign bit */
+		if (Nr != 0) {
+			Sign = get_bit(); // Read Sign (Table 10.13)
+			if (Sign) {
+				Nr = -Nr;
+			}
+		}
+		return Nr;
+	}
+
+	// Read DSD data section from DST input stream (Table 10.4)
+	void read_dsd_data(uint8_t* dsd_frame) {
+		for (auto i = 0u; i < MaxFrameLen * NrOfChannels; i++) {
+			dsd_frame[i] = (uint8_t)get_uint(8);
+		}
+	}
+
+	// Read segmentation for Filters and Ptables (Table 10.5)
+	void read_table_segmentation(unsigned int MaxNrOfSegments, unsigned int MinSegmentLength, segment_t& Segment, bool& SameSegmentForAllChannels) {
+		bool ResolutionRead = false;
+		unsigned int ChNr = 0;
+		unsigned int DefinedBits = 0;
+		unsigned int SegmentNr = 0;
+		unsigned int NrOfBits;
+		bool EndOfChannelSegment;
+		auto MaxSegmentSize = MaxFrameLen - MinSegmentLength / 8;
+		SameSegmentForAllChannels = get_bit(); // Read Same_Segm_For_All_Channels (Table 10.6)
+		if (SameSegmentForAllChannels) {
+			EndOfChannelSegment = get_bit(); // Read End_Of_Channel_Segm (Table 10.7)
+			while (!EndOfChannelSegment) {
+				if (SegmentNr >= MaxNrOfSegments) {
+					kodiLog(ADDON_LOG_ERROR, "Too many segments for this channel");
+					return;
+				}
+				if (!ResolutionRead) {
+					NrOfBits = log2_round_up(MaxFrameLen - MinSegmentLength / 8);
+					Segment.Resolution = get_uint(NrOfBits); // Read Resolution (Table 10.7)
+					if ((Segment.Resolution == 0) || (Segment.Resolution > MaxFrameLen - MinSegmentLength / 8)) {
+						kodiLog(ADDON_LOG_ERROR, "Invalid segment resolution");
+						return;
+					}
+					ResolutionRead = true;
+				}
+				NrOfBits = log2_round_up(MaxSegmentSize / Segment.Resolution);
+				Segment.SegmentLength[0][SegmentNr] = get_uint(NrOfBits); // Read Scaled_Length (Table 10.7)
+				if ((Segment.Resolution * 8 * Segment.SegmentLength[0][SegmentNr] < MinSegmentLength) || (Segment.Resolution * 8 * Segment.SegmentLength[0][SegmentNr] > MaxFrameLen * 8 - DefinedBits - MinSegmentLength)) {
+					kodiLog(ADDON_LOG_ERROR, "Invalid segment length");
+					return;
+				}
+				DefinedBits += Segment.Resolution * 8 * Segment.SegmentLength[0][SegmentNr];
+				MaxSegmentSize -= Segment.Resolution * Segment.SegmentLength[0][SegmentNr];
+				SegmentNr++;
+				EndOfChannelSegment = get_bit(); // Read End_Of_Channel_Segm (Table 10.7)
+			}
+			Segment.NrOfSegments[0] = SegmentNr + 1;
+			Segment.SegmentLength[0][SegmentNr] = 0;
+			for (ChNr = 1; ChNr < NrOfChannels; ChNr++) {
+				Segment.NrOfSegments[ChNr] = Segment.NrOfSegments[0];
+				for (SegmentNr = 0; SegmentNr < Segment.NrOfSegments[0]; SegmentNr++) {
+					Segment.SegmentLength[ChNr][SegmentNr] = Segment.SegmentLength[0][SegmentNr];
+				}
+			}
+		}
+		else {
+			while (ChNr < NrOfChannels) {
+				if (SegmentNr >= MaxNrOfSegments) {
+					kodiLog(ADDON_LOG_ERROR, "Too many segments for this channel");
+					return;
+				}
+				EndOfChannelSegment = get_bit(); // Read End_Of_Channel_Segm (Table 10.7)
+				if (!EndOfChannelSegment) {
+					if (!ResolutionRead) {
+						NrOfBits = log2_round_up(MaxFrameLen - MinSegmentLength / 8);
+						Segment.Resolution = get_uint(NrOfBits); // Read Resolution (Table 10.7)
+						if ((Segment.Resolution == 0) || (Segment.Resolution > MaxFrameLen - MinSegmentLength / 8)) {
+							kodiLog(ADDON_LOG_ERROR, "Invalid segment resolution");
+							return;
+						}
+						ResolutionRead = true;
+					}
+					NrOfBits = log2_round_up(MaxSegmentSize / Segment.Resolution);
+					Segment.SegmentLength[ChNr][SegmentNr] = get_uint(NrOfBits); // Read Scaled_Length (Table 10.7)
+					if ((Segment.Resolution * 8 * Segment.SegmentLength[ChNr][SegmentNr] < MinSegmentLength) || (Segment.Resolution * 8 * Segment.SegmentLength[ChNr][SegmentNr] > MaxFrameLen * 8 - DefinedBits - MinSegmentLength)) {
+						kodiLog(ADDON_LOG_ERROR, "Invalid segment length");
+						return;
+					}
+					DefinedBits += Segment.Resolution * 8 * Segment.SegmentLength[ChNr][SegmentNr];
+					MaxSegmentSize -= Segment.Resolution * Segment.SegmentLength[ChNr][SegmentNr];
+					SegmentNr++;
+				}
+				else {
+					Segment.NrOfSegments[ChNr] = SegmentNr + 1;
+					Segment.SegmentLength[ChNr][SegmentNr] = 0;
+					SegmentNr = 0;
+					DefinedBits = 0;
+					MaxSegmentSize = MaxFrameLen - MinSegmentLength / 8;
+					ChNr++;
+				}
+			}
+		}
+		if (!ResolutionRead) {
+			Segment.Resolution = 1;
+		}
+	}
+
+	// Copy segmentation for Filters and Ptables (Table 10.5)
+	void copy_table_segmentation() {
+		PSegment.Resolution = FSegment.Resolution;
+		PSameSegAllCh = true;
+		for (auto ChNr = 0u; ChNr < NrOfChannels; ChNr++) {
+			PSegment.NrOfSegments[ChNr] = FSegment.NrOfSegments[ChNr];
+			if (PSegment.NrOfSegments[ChNr] > MAXNROF_PSEGS) {
+				kodiLog(ADDON_LOG_ERROR, "Too many segments");
+				return;
+			}
+			if (PSegment.NrOfSegments[ChNr] != PSegment.NrOfSegments[0]) {
+				PSameSegAllCh = false;
+			}
+			for (auto SegmentNr = 0u; SegmentNr < FSegment.NrOfSegments[ChNr]; SegmentNr++) {
+				PSegment.SegmentLength[ChNr][SegmentNr] = FSegment.SegmentLength[ChNr][SegmentNr];
+				if ((PSegment.SegmentLength[ChNr][SegmentNr] != 0) && (PSegment.Resolution * 8 * PSegment.SegmentLength[ChNr][SegmentNr] < MIN_PSEG_LEN)) {
+					kodiLog(ADDON_LOG_ERROR, "Invalid segment length");
+					return;
+				}
+				if (PSegment.SegmentLength[ChNr][SegmentNr] != PSegment.SegmentLength[0][SegmentNr]) {
+					PSameSegAllCh = false;
+				}
+			}
+		}
+	}
+
+	// Read segmentation for Filters and Ptables (Table 10.5)
+	void read_segmentation() {
+		PSameSegAsF = get_bit(); // Read Same_Segmentation (Table 10.5)
+		read_table_segmentation(MAXNROF_FSEGS, MIN_FSEG_LEN, FSegment, FSameSegAllCh);
+		if (PSameSegAsF) {
+			copy_table_segmentation();
+		}
+		else {
+			read_table_segmentation(MAXNROF_PSEGS, MIN_PSEG_LEN, PSegment, PSameSegAllCh);
+		}
+	}
+
+	// Read mapping for Filters or Ptables (Table 10.8)
+	void read_table_mapping(unsigned int MaxNrOfTables, segment_t& S, unsigned int& NrOfTables, bool& SameMapAllCh) {
+		unsigned int CountTables = 1;
+		unsigned int NrOfBits = 1;
+		S.Table4Segment[0][0] = 0;
+		SameMapAllCh = get_bit(); // Read Same_Maps_For_All_Channels (Table 10.9)
+		if (SameMapAllCh) {
+			for (auto SegmentNr = 1u; SegmentNr < S.NrOfSegments[0]; SegmentNr++) {
+				NrOfBits = log2_round_up(CountTables);
+				S.Table4Segment[0][SegmentNr] = get_uint(NrOfBits); // Read Element (Table 10.10)
+				if (S.Table4Segment[0][SegmentNr] == CountTables) {
+					CountTables++;
+				}
+				else if (S.Table4Segment[0][SegmentNr] > CountTables) {
+					kodiLog(ADDON_LOG_ERROR, "Invalid table number for segment");
+					return;
+				}
+			}
+			for (auto ChNr = 1u; ChNr < NrOfChannels; ChNr++) {
+				if (S.NrOfSegments[ChNr] != S.NrOfSegments[0]) {
+					kodiLog(ADDON_LOG_ERROR, "Mapping can not be the same for all channels");
+					return;
+				}
+				for (auto SegmentNr = 0u; SegmentNr < S.NrOfSegments[0]; SegmentNr++) {
+					S.Table4Segment[ChNr][SegmentNr] = S.Table4Segment[0][SegmentNr];
+				}
+			}
+		}
+		else {
+			for (auto ChNr = 0u; ChNr < NrOfChannels; ChNr++) {
+				for (auto SegmentNr = 0u; SegmentNr < S.NrOfSegments[ChNr]; SegmentNr++) {
+					if ((ChNr != 0) || (SegmentNr != 0)) {
+						NrOfBits = log2_round_up(CountTables);
+						S.Table4Segment[ChNr][SegmentNr] = get_uint(NrOfBits); // Read Element (Table 10.10)
+						if (S.Table4Segment[ChNr][SegmentNr] == CountTables) {
+							CountTables++;
+						}
+						else if (S.Table4Segment[ChNr][SegmentNr] > CountTables) {
+							kodiLog(ADDON_LOG_ERROR, "Invalid table number for segment");
+							return;
+						}
+					}
+				}
+			}
+		}
+		if (CountTables > MaxNrOfTables) {
+			kodiLog(ADDON_LOG_ERROR, "Too many tables for this frame");
+			return;
+		}
+		NrOfTables = CountTables;
+	}
+
+	// Copy mapping for Ptables from Filter
+	void copy_table_mapping() {
+		PSameMapAllCh = true;
+		for (auto ChNr = 0u; ChNr < NrOfChannels; ChNr++) {
+			if (PSegment.NrOfSegments[ChNr] == FSegment.NrOfSegments[ChNr]) {
+				for (auto SegmentNr = 0u; SegmentNr < FSegment.NrOfSegments[ChNr]; SegmentNr++) {
+					PSegment.Table4Segment[ChNr][SegmentNr] = FSegment.Table4Segment[ChNr][SegmentNr];
+					if (PSegment.Table4Segment[ChNr][SegmentNr] != PSegment.Table4Segment[0][SegmentNr]) {
+						PSameMapAllCh = false;
+					}
+				}
+			}
+			else {
+				kodiLog(ADDON_LOG_ERROR, "Not the same number of segments for Filters and Ptables");
+				return;
+			}
+		}
+		NrOfPtables = NrOfFilters;
+		if (NrOfPtables > MaxNrOfPtables) {
+			kodiLog(ADDON_LOG_ERROR, "Too many tables for this frame");
+			return;
+		}
+	}
+
+	// Read mapping (which channel uses which Filter/Ptable)
+	void read_mapping() {
+		PSameMapAsF = get_bit(); // Read Same_Mapping (Table 10.8)
+		read_table_mapping(MaxNrOfFilters, FSegment, NrOfFilters, FSameMapAllCh);
+		if (PSameMapAsF) {
+			copy_table_mapping();
+		}
+		else {
+			read_table_mapping(MaxNrOfPtables, PSegment, NrOfPtables, PSameMapAllCh);
+		}
+		for (auto ChNr = 0u; ChNr < NrOfChannels; ChNr++) {
+			HalfProb[ChNr] = get_bit();
+		}
+	}
+
+	// Read Filter data from the DST stream (Table 10.13):
+	// - which channel uses which Filter
+	// - for each filter:
+	//   - prediction order
+	//   - coefficients
+	void read_filter_coef_sets(ft_t& CF) {
+		// Read the filter parameters
+		for (auto FilterNr = 0u; FilterNr < NrOfFilters; FilterNr++) {
+			PredOrder[FilterNr] = get_uint(SIZE_CODEDPREDORDER); // Read Coded_Pred_Order (Table 10.13)
+			PredOrder[FilterNr]++;
+			CF.Coded[FilterNr] = get_bit(); // Read Coded_Filter_Coef_Set (Table 10.13)
+			if (!CF.Coded[FilterNr]) {
+				CF.BestMethod[FilterNr] = (unsigned int)-1;
+				for (auto CoefNr = 0u; CoefNr < PredOrder[FilterNr]; CoefNr++) {
+					ICoefA[FilterNr][CoefNr] = (int16_t)get_sint(SIZE_PREDCOEF); // Read Coef (Table 10.13)
+				}
+			}
+			else {
+				CF.BestMethod[FilterNr] = get_uint(SIZE_RICEMETHOD); // Read CC_Method (Table 10.13)
+				auto bestmethod = CF.BestMethod[FilterNr];
+				if (CF.CPredOrder[bestmethod] >= PredOrder[FilterNr]) {
+					kodiLog(ADDON_LOG_ERROR, "Invalid coefficient coding method");
+					return;
+				}
+				for (auto CoefNr = 0u; CoefNr < CF.CPredOrder[bestmethod]; CoefNr++) {
+					ICoefA[FilterNr][CoefNr] = (int16_t)get_sint(SIZE_PREDCOEF); // Read Coef (Table 10.13)
+				}
+				CF.m[FilterNr][bestmethod] = get_uint(SIZE_RICEM); // Read CCM (Table 10.13)
+				for (auto CoefNr = CF.CPredOrder[bestmethod]; CoefNr < PredOrder[FilterNr]; CoefNr++) {
+					int x = 0;
+					int c;
+					for (auto TapNr = 0u; TapNr < CF.CPredOrder[bestmethod]; TapNr++) {
+						x += CF.CPredCoef[bestmethod][TapNr] * ICoefA[FilterNr][CoefNr - TapNr - 1];
+					}
+					if (x >= 0) {
+						c = rice_decode(CF.m[FilterNr][bestmethod]) - (x + 4) / 8;
+					}
+					else {
+						c = rice_decode(CF.m[FilterNr][bestmethod]) + (-x + 3) / 8;
+					}
+					if ((c < -(1 << (SIZE_PREDCOEF - 1))) || (c >= (1 << (SIZE_PREDCOEF - 1)))) {
+						kodiLog(ADDON_LOG_ERROR, "filter coefficient out of range");
+						return;
+					}
+					else {
+						ICoefA[FilterNr][CoefNr] = (int16_t)c;
+					}
+				}
+			}
+		}
+		for (auto ChNr = 0u; ChNr < NrOfChannels; ChNr++) {
+			NrOfHalfBits[ChNr] = PredOrder[FSegment.Table4Segment[ChNr][0]];
+		}
+	}
+
+	// Read Ptable data from the DST stream (Table 10.14):
+	// - which channel uses which Ptable
+	// - for each Ptable all entries
+	void read_probability_tables(pt_t& CP, vector<array<unsigned int, AC_HISMAX>>& P_one) {
+		// Read the data of probability tables (table entries)
+		for (auto PtableNr = 0u; PtableNr < NrOfPtables; PtableNr++) {
+			PtableLen[PtableNr] = get_uint(AC_HISBITS); // Read Coded_Ptable_Len (Table 10.14)
+			PtableLen[PtableNr]++;
+			if (PtableLen[PtableNr] > 1) {
+				CP.Coded[PtableNr] = get_bit(); // Read Coded_Ptable (Table 10.14)
+				if (!CP.Coded[PtableNr]) {
+					CP.BestMethod[PtableNr] = (unsigned int)-1;
+					for (auto EntryNr = 0u; EntryNr < PtableLen[PtableNr]; EntryNr++) {
+						P_one[PtableNr][EntryNr] = get_uint(AC_BITS - 1); // Read Coded_P_one (Table 10.14)
+						P_one[PtableNr][EntryNr]++;
+					}
+				}
+				else {
+					CP.BestMethod[PtableNr] = get_uint(SIZE_RICEMETHOD); // Read PC_Method (Table 10.14)
+					auto bestmethod = CP.BestMethod[PtableNr];
+					if (CP.CPredOrder[bestmethod] >= PtableLen[PtableNr]) {
+						kodiLog(ADDON_LOG_ERROR, "Invalid Ptable coding method");
+						return;
+					}
+					for (auto EntryNr = 0u; EntryNr < CP.CPredOrder[bestmethod]; EntryNr++) {
+						P_one[PtableNr][EntryNr] = get_uint(AC_BITS - 1); // Read Coded_P_one (Table 10.14)
+						P_one[PtableNr][EntryNr]++;
+					}
+					CP.m[PtableNr][bestmethod] = get_uint(SIZE_RICEM); // Read PCM (Table 10.14)
+					for (auto EntryNr = CP.CPredOrder[bestmethod]; EntryNr < PtableLen[PtableNr]; EntryNr++) {
+						int x = 0;
+						int c;
+						for (auto TapNr = 0u; TapNr < CP.CPredOrder[bestmethod]; TapNr++) {
+							x += CP.CPredCoef[bestmethod][TapNr] * P_one[PtableNr][EntryNr - TapNr - 1];
+						}
+						if (x >= 0) {
+							c = rice_decode(CP.m[PtableNr][bestmethod]) - (x + 4) / 8;
+						}
+						else {
+							c = rice_decode(CP.m[PtableNr][bestmethod]) + (-x + 3) / 8;
+						}
+						if ((c < 1) || (c > (1 << (AC_BITS - 1)))) {
+							kodiLog(ADDON_LOG_ERROR, "Ptable entry out of range");
+							return;
+						}
+						else {
+							P_one[PtableNr][EntryNr] = (unsigned int)c;
+						}
+					}
+				}
+			}
+			else {
+				P_one[PtableNr][0] = 128;
+				CP.BestMethod[PtableNr] = (unsigned int)-1;
+			}
+		}
+	}
+
+	// Read arithmetic coded data from the DST stream (Table 10.12):
+	// - length of the arithmetic code
+	// - all bits of the arithmetic code
+	void read_arithmetic_coded_data(uint8_t* AData) {
+		auto ADataLen = CalcNrOfBits - get_offset();
+		for (auto j = 0u; j < (ADataLen >> 3); j++) {
+			AData[j] = (uint8_t)get_uint(8); // Read A_Data (Table 10.12)
+		}
+		uint8_t Val = 0;
+		for (auto j = ADataLen & ~7; j < ADataLen; j++) {
+			uint8_t v = get_bit(); // Read A_Data (Table 10.12)
+			Val |= (uint8_t)(v << (7 - (j & 7)));
+			if (j == ADataLen - 1) {
+				AData[j >> 3] = Val;
+				Val = 0;
+			}
+		}
+	}
+
+};
+
+}
+
+#endif
+
+

+ 36 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/segment.h

@@ -0,0 +1,36 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef SEGMENT_H
+#define SEGMENT_H
+
+#include <stdint.h>
+#include <array>
+#include <vector>
+#include "consts.h"
+
+using std::array;
+using std::vector;
+
+namespace dst
+{
+
+class segment_t {
+public:
+	unsigned int Resolution;                                 // Resolution for segments
+	vector<array<unsigned int, MAXNROF_SEGS>> SegmentLength; // SegmentLength[ChNr][SegmentNr]
+	vector<unsigned int> NrOfSegments;                       // NrOfSegments[ChNr]
+	vector<array<unsigned int, MAXNROF_SEGS>> Table4Segment; // Table4Segment[ChNr][SegmentNr]
+public:
+	void init(unsigned int channels) {
+		SegmentLength.resize(channels);
+		NrOfSegments.resize(channels);
+		Table4Segment.resize(channels);
+	}
+};
+
+}
+
+#endif

+ 79 - 0
entry/src/main/cpp/third_party/sacd/libdstdec/stream.h

@@ -0,0 +1,79 @@
+/*
+* Direct Stream Transfer (DST) codec
+* ISO/IEC 14496-3 Part 3 Subpart 10: Technical description of lossless coding of oversampled audio
+*/
+
+#ifndef STREAM_H
+#define STREAM_H
+
+#include <stdint.h>
+#include <algorithm>
+#include "common.h"
+#include "consts.h"
+
+using std::min;
+
+namespace dst
+{
+
+class stream_t {
+	const uint8_t* m_data;
+	unsigned int   m_size;
+	unsigned int   m_offset;
+public:
+	stream_t() {
+		m_data = nullptr;
+		m_size = 0;
+		m_offset = 0;
+	}
+
+	void set_data(const uint8_t* data, size_t size) {
+		m_data = data;
+		m_size = size;
+		m_offset = 0;
+	}
+
+	unsigned int get_offset() {
+		return m_offset;
+	}
+
+	bool get_bit() {
+		if (m_offset + 1 > 8 * m_size) {
+			kodiLog(ADDON_LOG_ERROR, "read after end of stream");
+			return false;
+		}
+		uint32_t value = m_data[m_offset / 8];
+		value >>= 7 - m_offset % 8;
+		value &= 1;
+		m_offset++;
+		return (bool)value;
+	}
+
+	int get_sint(unsigned int length) {
+		int value = (int)get_uint(length);
+		if (value >= (1 << (length - 1))) {
+			value -= (1 << length);
+		}
+		return value;
+	}
+
+	uint32_t get_uint(unsigned int length) {
+		if (m_offset + length > 8 * m_size) {
+			kodiLog(ADDON_LOG_ERROR, "read after end of stream");
+			return 0;
+		}
+		uint32_t value = 0;
+		for (auto i = 0u; i < (m_offset % 8 + length + 7) / 8; i++) {
+			value = (value << 8) | m_data[m_offset / 8 + i];
+		}
+		value >>= 7 - (m_offset + length - 1) % 8;
+		value &= (1 << length) - 1;
+		m_offset += length;
+		return value;
+	}
+
+};
+
+}
+
+#endif

+ 1 - 7
entry/src/main/ets/common/constants/CommonConstants.ets

@@ -81,13 +81,7 @@ export class CommonConstants {
     ,'.aif','.au','.eac3','.mlp','.tak','.thd','.tta','.wv','.ac3','.amr','.mka','.mpc','.ra',
     '.asf','.m2ts','.m4b','.mts','.rm','.wtv','.dts','.av3a','.dsd','.dsf','.wav']
 
-  // 图片帧信息常量
-  static readonly IMAGE_FRAME_INFO: ImageFrameInfo[] = [
-    { src: $r("app.media.app_loading0") },
-    { src: $r("app.media.app_loading1") },
-    { src: $r("app.media.app_loading2") },
-    { src: $r("app.media.app_loading3") },
-  ];
+
 
   // 倍数格式支持列表
   public static video_speed_list: number[] = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3]

+ 1 - 0
entry/src/main/ets/common/enums/RemoteDriveManagerStates.ets

@@ -41,6 +41,7 @@ export enum RemoteDriveManagerStates{
   UploadStart = "UploadStart",
   UploadProgress = "UploadProgress",
   UploadSuccess = "UploadSuccess",
+  UploadSkipped = "UploadSkipped",
   UploadFailed = "UploadFailed",
   UploadPaused = "UploadPaused",
   UploadResumed = "UploadResumed",

+ 1 - 3
entry/src/main/ets/common/util/BackgroundManager.ets

@@ -41,9 +41,7 @@ export class BackgroundManager {
   // 更新数据传输连续任务
   public async updateDataTransferContinuousTask(): Promise<void> {
     try {
-      // 简化版:仅记录日志,不实际更新后台任务
-      // 实际应用中应该更新任务进度通知
-      Logger.info(TAG, '更新数据传输任务');
+      // 目录列表请求阶段会高频触发,这里保持空实现,避免日志反复占用主线程。
     } catch (error) {
       Logger.error(TAG, `更新数据传输任务失败: ${error}`);
     }

+ 494 - 77
entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets

@@ -1,4 +1,4 @@
-import { taskpool } from '@kit.ArkTS';
+import { taskpool, util } from '@kit.ArkTS';
 import fileIo from '@ohos.file.fs';
 import nativeBridge from 'libentry.so';
 import Logger from './Logger';
@@ -20,6 +20,8 @@ const SACD_FRAME_SIZE_64: number = 4704;
 const SACD_FRAME_RATE: number = 75;
 const SACD_MAX_PACKET_COUNT: number = 7;
 const SACD_MAX_FRAME_BUFFER_SIZE: number = 64 * 1024;
+const SACD_DST_BATCH_MAX_FRAMES: number = 64;
+const SACD_DST_BATCH_MAX_BYTES: number = 256 * 1024;
 
 const SACD_PACKET_TYPE_AUDIO: number = 2;
 
@@ -34,11 +36,23 @@ const DSF_CHANNEL_TYPE_5_1_CHANNELS: number = 7;
 
 const SACD_FRAME_FORMAT_DST: number = 0;
 const BIT_REVERSE_TABLE: Uint8Array = createBitReverseTable();
+const ISO_LISTABLE_MEDIA_EXTENSIONS: string[] = [
+  '.mp3', '.wav', '.wma', '.mp2', '.mov', '.flac', '.midi', '.ra', '.aac', '.ape', '.cda', '.alac', '.m4a', '.mp4',
+  '.wmv', '.mkv', '.3gv', '.m4v', '.avi', '.rmvb', '.flv', '.3g2', '.mpg', '.webm', '.ogv', '.f4v', '.swf', '.dsf',
+  '.vob', '.3gp', '.mpeg', '.ts', '.ogg', '.opus', '.wv', '.aiff', '.amr', '.aif', '.dff', '.au', '.eac3', '.mlp',
+  '.tak', '.thd', '.tta', '.ac3', '.mka', '.mpc', '.dts', '.asf', '.m2ts', '.m4b', '.mts', '.rm', '.wtv', '.av3a',
+  '.dsd'
+];
 
-const AUDIO_EXTENSIONS: string[] = [
-  '.mp3', '.wav', '.wma', '.mp2', '.mov', '.flac', '.midi', '.ra', '.aac', '.ape', '.cda', '.alac', '.m4a',
-  '.ogg', '.opus', '.wv', '.aiff', '.amr', '.aif', '.dff', '.au', '.eac3', '.mlp', '.tak', '.thd', '.tta',
-  '.ac3', '.mka', '.mpc', '.asf', '.m2ts', '.m4b', '.mts', '.rm', '.wtv', '.dts', '.av3a', '.dsd', '.dsf'
+const SACD_CHARSET_LABELS: string[] = [
+  'us-ascii',
+  'us-ascii',
+  'iso-8859-1',
+  'shift_jis',
+  'euc-kr',
+  'gb18030',
+  'big5',
+  'iso-8859-1'
 ];
 
 interface IsoDirectoryRootDescriptor {
@@ -68,9 +82,28 @@ interface SacdMasterTocInfo {
   area2TocSize: number;
 }
 
+interface SacdMasterTextInfo {
+  albumTitle: string;
+  albumArtist: string;
+  discTitle: string;
+  discArtist: string;
+}
+
+interface SacdTrackTextInfo {
+  title: string;
+  performer: string;
+  songwriter: string;
+  composer: string;
+  arranger: string;
+  message: string;
+  extraMessage: string;
+}
+
 interface SacdAreaTocInfo {
   trackOffset: number;
   trackCount: number;
+  trackStartLsn: number;
+  trackEndLsn: number;
   channelCount?: number;
   frameFormat?: number;
 }
@@ -89,6 +122,7 @@ interface SacdPacketInfo {
 }
 
 interface SacdFrameInfo {
+  timecodeFrames: number;
   sectorCount: number;
   channelBit2?: boolean;
   channelBit3?: boolean;
@@ -98,11 +132,25 @@ interface SacdFrameAssembler {
   started: boolean;
   size: number;
   channelCount: number;
+  timecodeFrames: number;
   dstEncoded: boolean;
   sectorCount: number;
   buffer: Uint8Array;
 }
 
+interface SacdTrackFrameFilter {
+  enabled: boolean;
+  startFrame: number;
+  endFrameExclusive: number;
+}
+
+interface SacdDstFrameBatch {
+  totalPayloadBytes: number;
+  frameCount: number;
+  frameLengths: number[];
+  frameChunks: Uint8Array[];
+}
+
 interface DsfWriterState {
   file: fileIo.File;
   channelCount: number;
@@ -135,6 +183,11 @@ export interface IsoArchiveHelperAudioEntry {
   areaType?: string;
   channelCount?: number;
   frameFormat?: number;
+  title?: string;
+  album?: string;
+  artist?: string;
+  albumArtist?: string;
+  comment?: string;
 }
 
 export interface IsoArchiveHelperExtractTaskResult {
@@ -161,6 +214,7 @@ export type IsoArchiveExtractProgressReporter = (message: IsoArchiveHelperExtrac
 interface SacdDstNativeModule {
   createSacdDstDecoder(channelCount: number, samplerate: number, framerate: number): number;
   decodeSacdDstFrame(decoderId: number, dstFrame: ArrayBuffer): ArrayBuffer;
+  decodeSacdDstFrames(decoderId: number, payload: ArrayBuffer, frameLengths: ArrayBuffer): ArrayBuffer;
   releaseSacdDstDecoder(decoderId: number): void;
 }
 
@@ -179,11 +233,32 @@ interface IsoEntryExtractResult {
 
 export function listIsoAudioEntriesCore(isoPath: string): IsoArchiveHelperAudioEntry[] {
   Logger.info('IsoArchiveTaskHelper', `heanup ISO list start path=${isoPath}`);
-  const standardEntries: IsoArchiveHelperAudioEntry[] = listStandardIsoAudioEntries(isoPath);
-  if (standardEntries.length > 0) {
-    return standardEntries;
+  const sacdEntries: IsoArchiveHelperAudioEntry[] = listSacdAudioEntries(isoPath);
+  if (sacdEntries.length > 0) {
+    return sacdEntries;
+  }
+  if (isSacdIsoPath(isoPath)) {
+    Logger.warn('IsoArchiveTaskHelper', `heanup SACD ISO detected but no track entries parsed path=${isoPath}`);
+    return [];
+  }
+  return listStandardIsoEntries(isoPath);
+}
+
+function isSacdIsoPath(isoPath: string): boolean {
+  let isoFile: fileIo.File | undefined = undefined;
+  try {
+    isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
+    return parseSacdMasterToc(isoFile) !== null;
+  } catch (_error) {
+    return false;
+  } finally {
+    if (isoFile) {
+      try {
+        fileIo.closeSync(isoFile);
+      } catch (_error) {
+      }
+    }
   }
-  return listSacdAudioEntries(isoPath);
 }
 
 export function extractIsoAudioEntriesCore(payloadJson: string,
@@ -256,7 +331,8 @@ export function extractIsoAudioEntriesCore(payloadJson: string,
     }
     if (progressState) {
       progressState.completedUnits = progressState.totalUnits;
-      reportIsoExtractProgress(progressState, '', 'done', true, progressReporter);
+      const finalStage: string = result.failedEntries.length > 0 ? 'error' : 'done';
+      reportIsoExtractProgress(progressState, '', finalStage, true, progressReporter);
     }
   } catch (_error) {
     for (let index: number = 0; index < payload.entries.length; index++) {
@@ -337,6 +413,9 @@ function reportIsoExtractProgress(progressState: IsoExtractProgressState | undef
   } else {
     progress = Math.ceil((progressState.completedUnits / progressState.totalUnits) * 100);
   }
+  if (stage !== 'done' && progress >= 100) {
+    progress = 99;
+  }
   if (!force && progress <= progressState.lastProgress) {
     return;
   }
@@ -358,7 +437,7 @@ function reportIsoExtractProgress(progressState: IsoExtractProgressState | undef
   }
 }
 
-function listStandardIsoAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
+function listStandardIsoEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
   let isoFile: fileIo.File | undefined = undefined;
   try {
     isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
@@ -492,7 +571,7 @@ function collectIsoAudioEntries(isoFile: fileIo.File, rootDescriptor: IsoDirecto
         continue;
       }
 
-      if (!isAudioFileName(record.name)) {
+      if (!isListableMediaFile(record.name)) {
         continue;
       }
 
@@ -585,16 +664,6 @@ function decodeJolietString(bytes: Uint8Array): string {
   return result;
 }
 
-function isAudioFileName(fileName: string): boolean {
-  const extension: string = getFileExtension(fileName).toLowerCase();
-  for (let index: number = 0; index < AUDIO_EXTENSIONS.length; index++) {
-    if (extension === AUDIO_EXTENSIONS[index]) {
-      return true;
-    }
-  }
-  return false;
-}
-
 function getFileExtension(fileName: string): string {
   const lastDot: number = fileName.lastIndexOf('.');
   if (lastDot < 0) {
@@ -603,6 +672,16 @@ function getFileExtension(fileName: string): string {
   return fileName.substring(lastDot);
 }
 
+function isListableMediaFile(fileName: string): boolean {
+  const extension: string = getFileExtension(fileName).toLowerCase();
+  for (let index: number = 0; index < ISO_LISTABLE_MEDIA_EXTENSIONS.length; index++) {
+    if (extension === ISO_LISTABLE_MEDIA_EXTENSIONS[index]) {
+      return true;
+    }
+  }
+  return false;
+}
+
 function extractStandardIsoEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
   progressState?: IsoExtractProgressState, progressReporter?: IsoArchiveExtractProgressReporter): IsoEntryExtractResult {
   if (entry.extent <= 0 || entry.size <= 0) {
@@ -668,18 +747,18 @@ function listSacdAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
     if (!masterToc) {
       return [];
     }
+    const masterText: SacdMasterTextInfo = parseSacdMasterText(isoFile);
 
     const stereoEntries: IsoArchiveHelperAudioEntry[] = parseSacdAreaEntries(
       isoFile,
       masterToc.area1Toc1Start,
       masterToc.area1TocSize,
-      '2CH'
+      '2CH',
+      masterText
     );
-    if (stereoEntries.length > 0) {
-      return stereoEntries;
-    }
-
-    return parseSacdAreaEntries(isoFile, masterToc.area2Toc1Start, masterToc.area2TocSize, 'MCH');
+    const multichannelEntries: IsoArchiveHelperAudioEntry[] =
+      parseSacdAreaEntries(isoFile, masterToc.area2Toc1Start, masterToc.area2TocSize, 'MCH', masterText);
+    return stereoEntries.concat(multichannelEntries);
   } catch (_error) {
     return [];
   } finally {
@@ -711,7 +790,7 @@ function parseSacdMasterToc(isoFile: fileIo.File): SacdMasterTocInfo | null {
 }
 
 function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTocSize: number,
-  areaType: string): IsoArchiveHelperAudioEntry[] {
+  areaType: string, masterText: SacdMasterTextInfo): IsoArchiveHelperAudioEntry[] {
   if (areaStartLsn <= 0 || areaTocSize <= 0) {
     return [];
   }
@@ -729,12 +808,15 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
   const areaInfo: SacdAreaTocInfo = {
     trackOffset: areaData[68],
     trackCount: areaData[69],
+    trackStartLsn: readUInt32BE(areaData, 72),
+    trackEndLsn: readUInt32BE(areaData, 76),
     channelCount: areaData[32],
     frameFormat: areaData[21] & 0x0f
   };
-  if (areaInfo.trackCount <= 0) {
+  if (areaInfo.trackCount <= 0 || areaInfo.trackStartLsn <= 0 || areaInfo.trackEndLsn < areaInfo.trackStartLsn) {
     return [];
   }
+  const trackTextList: SacdTrackTextInfo[] = parseSacdAreaTrackTexts(areaData, areaInfo.trackCount);
 
   let trackListOffsetBlock: Uint8Array | null = null;
   let trackListTimeBlock: Uint8Array | null = null;
@@ -757,14 +839,17 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
   const result: IsoArchiveHelperAudioEntry[] = [];
   const maxTrackCount: number = areaInfo.trackCount > 255 ? 255 : areaInfo.trackCount;
   for (let index: number = 0; index < maxTrackCount; index++) {
-    const startLsn: number = readUInt32BE(offsetBlock, 8 + index * 4);
+    const listedStartLsn: number = readUInt32BE(offsetBlock, 8 + index * 4);
+    const startLsn: number = index > 0 ? listedStartLsn : areaInfo.trackStartLsn;
     const rawLengthLsn: number = readUInt32BE(offsetBlock, 8 + 255 * 4 + index * 4);
     let lengthLsn: number = rawLengthLsn;
     if (index + 1 < maxTrackCount) {
       const nextStartLsn: number = readUInt32BE(offsetBlock, 8 + (index + 1) * 4);
       if (nextStartLsn > startLsn) {
-        lengthLsn = nextStartLsn - startLsn;
+        lengthLsn = nextStartLsn - startLsn + 1;
       }
+    } else if (areaInfo.trackEndLsn >= startLsn) {
+      lengthLsn = areaInfo.trackEndLsn - startLsn + 1;
     }
     if (startLsn <= 0 || lengthLsn <= 0) {
       continue;
@@ -778,12 +863,33 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
     }
 
     const trackNumber: number = areaInfo.trackOffset > 0 ? areaInfo.trackOffset + index : index + 1;
-    const trackLabel: string = areaType === 'MCH' ? `MCH ${padTrackNumber(trackNumber)}. Track ${padTrackNumber(trackNumber)}.dsf`
-      : `${padTrackNumber(trackNumber)}. Track ${padTrackNumber(trackNumber)}.dsf`;
+    const trackText: SacdTrackTextInfo = index < trackTextList.length ? trackTextList[index] : createEmptySacdTrackText();
+    const trackTitle: string = firstNonEmptySacdText([
+      trackText.title,
+      trackText.message,
+      `Track ${padTrackNumber(trackNumber)}`
+    ]);
+    const safeTrackTitle: string = sanitizeSacdPathSegment(trackTitle);
+    const displayLabel: string = `${padTrackNumber(trackNumber)}. ${safeTrackTitle}.dsf`;
+    const trackPath: string = areaType === 'MCH' ? `MCH/${displayLabel}` : displayLabel;
+    const outputName: string = `${trackTitle}.dsf`;
+    const albumTitle: string = firstNonEmptySacdText([
+      masterText.albumTitle,
+      masterText.discTitle
+    ]);
+    const artistName: string = firstNonEmptySacdText([
+      trackText.performer,
+      masterText.albumArtist,
+      masterText.discArtist
+    ]);
+    const commentText: string = firstNonEmptySacdText([
+      trackText.message,
+      trackText.extraMessage
+    ]);
 
     result.push({
-      path: trackLabel,
-      name: trackLabel,
+      path: trackPath,
+      name: outputName,
       size: lengthLsn * ISO_SECTOR_SIZE,
       extent: startLsn,
       entryType: 'sacd_track',
@@ -794,7 +900,12 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
       durationFrames,
       areaType,
       channelCount: areaInfo.channelCount,
-      frameFormat: areaInfo.frameFormat
+      frameFormat: areaInfo.frameFormat,
+      title: trackTitle,
+      album: albumTitle,
+      artist: artistName,
+      albumArtist: firstNonEmptySacdText([masterText.albumArtist, masterText.discArtist]),
+      comment: commentText
     });
     Logger.info('IsoArchiveTaskHelper',
       `heanup SACD track meta index=${index}, startLsn=${startLsn}, rawLengthLsn=${rawLengthLsn}, ` +
@@ -814,6 +925,186 @@ function readSacdTrackTimeFrames(trackListTimeBlock: Uint8Array, offset: number)
   return minutes * 60 * 75 + seconds * 75 + frames;
 }
 
+function parseSacdMasterText(isoFile: fileIo.File): SacdMasterTextInfo {
+  const emptyText: SacdMasterTextInfo = createEmptySacdMasterText();
+  const masterData: Uint8Array = readBytes(isoFile, SACD_START_OF_MASTER_TOC * ISO_SECTOR_SIZE,
+    SACD_MASTER_TOC_LEN * ISO_SECTOR_SIZE);
+  if (masterData.length < ISO_SECTOR_SIZE * 2 || readAscii(masterData, 0, 8) !== 'SACDMTOC') {
+    return emptyText;
+  }
+  const textAreaCount: number = masterData[128];
+  if (textAreaCount <= 0) {
+    return emptyText;
+  }
+
+  const textBlockStart: number = ISO_SECTOR_SIZE;
+  const textBlock: Uint8Array = masterData.subarray(textBlockStart, textBlockStart + ISO_SECTOR_SIZE);
+  if (textBlock.length < ISO_SECTOR_SIZE || readAscii(textBlock, 0, 8) !== 'SACDText') {
+    return emptyText;
+  }
+
+  const charsetIndex: number = masterData[138] & 0x07;
+  return {
+    albumTitle: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 16), charsetIndex),
+    albumArtist: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 18), charsetIndex),
+    discTitle: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 32), charsetIndex),
+    discArtist: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 34), charsetIndex)
+  };
+}
+
+function parseSacdAreaTrackTexts(areaData: Uint8Array, trackCount: number): SacdTrackTextInfo[] {
+  const texts: SacdTrackTextInfo[] = [];
+  for (let index: number = 0; index < trackCount; index++) {
+    texts.push(createEmptySacdTrackText());
+  }
+  if (trackCount <= 0 || areaData.length < ISO_SECTOR_SIZE) {
+    return texts;
+  }
+
+  const charsetIndex: number = areaData.length > 90 ? (areaData[90] & 0x07) : 0;
+  const sectorCount: number = Math.floor(areaData.length / ISO_SECTOR_SIZE);
+  for (let sectorIndex: number = 0; sectorIndex < sectorCount; sectorIndex++) {
+    const offset: number = sectorIndex * ISO_SECTOR_SIZE;
+    if (readAscii(areaData, offset, 8) !== 'SACDTTxt') {
+      continue;
+    }
+    const textBlock: Uint8Array = areaData.subarray(offset, offset + ISO_SECTOR_SIZE);
+    for (let trackIndex: number = 0; trackIndex < trackCount; trackIndex++) {
+      const textOffset: number = readUInt16BE(textBlock, 8 + trackIndex * 2);
+      if (textOffset <= 0 || textOffset >= textBlock.length) {
+        continue;
+      }
+      let textPointer: number = textOffset;
+      const textAmount: number = textBlock[textPointer];
+      if (textAmount <= 0) {
+        continue;
+      }
+      textPointer += 4;
+      for (let textIndex: number = 0; textIndex < textAmount && textPointer < textBlock.length; textIndex++) {
+        const textType: number = textBlock[textPointer];
+        textPointer += 2;
+        const endOffset: number = findSacdCStringEnd(textBlock, textPointer);
+        const value: string = decodeSacdTextRange(textBlock, textPointer, endOffset, charsetIndex);
+        applySacdTrackText(texts[trackIndex], textType, value);
+        textPointer = endOffset;
+        while (textPointer < textBlock.length && textBlock[textPointer] === 0) {
+          textPointer++;
+        }
+      }
+    }
+    break;
+  }
+  return texts;
+}
+
+function createEmptySacdMasterText(): SacdMasterTextInfo {
+  return {
+    albumTitle: '',
+    albumArtist: '',
+    discTitle: '',
+    discArtist: ''
+  };
+}
+
+function createEmptySacdTrackText(): SacdTrackTextInfo {
+  return {
+    title: '',
+    performer: '',
+    songwriter: '',
+    composer: '',
+    arranger: '',
+    message: '',
+    extraMessage: ''
+  };
+}
+
+function readSacdTextByOffset(textBlock: Uint8Array, offset: number, charsetIndex: number): string {
+  if (offset <= 0 || offset >= textBlock.length) {
+    return '';
+  }
+  const endOffset: number = findSacdCStringEnd(textBlock, offset);
+  return decodeSacdTextRange(textBlock, offset, endOffset, charsetIndex);
+}
+
+function findSacdCStringEnd(buffer: Uint8Array, start: number): number {
+  let cursor: number = start;
+  while (cursor < buffer.length && buffer[cursor] !== 0) {
+    cursor++;
+  }
+  return cursor;
+}
+
+function decodeSacdTextRange(buffer: Uint8Array, start: number, end: number, charsetIndex: number): string {
+  if (start >= end || start < 0 || end > buffer.length) {
+    return '';
+  }
+  const bytes: Uint8Array = buffer.subarray(start, end);
+  const encoding: string = charsetIndex >= 0 && charsetIndex < SACD_CHARSET_LABELS.length ?
+    SACD_CHARSET_LABELS[charsetIndex] : 'utf-8';
+  try {
+    const decoder = new util.TextDecoder(encoding);
+    return sanitizeSacdText(decoder.decode(bytes));
+  } catch (_error) {
+    return sanitizeSacdText(decodeAsciiString(bytes));
+  }
+}
+
+function sanitizeSacdText(value: string): string {
+  if (!value) {
+    return '';
+  }
+  return value.replace(/\u0000/g, '').replace(/\s+/g, ' ').trim();
+}
+
+function sanitizeSacdPathSegment(value: string): string {
+  const sanitized: string = sanitizeSacdText(value).replace(/[\\/:*?"<>|]/g, '_').trim();
+  if (sanitized.length > 0) {
+    return sanitized;
+  }
+  return 'Track';
+}
+
+function applySacdTrackText(target: SacdTrackTextInfo, textType: number, value: string): void {
+  if (!value) {
+    return;
+  }
+  switch (textType) {
+    case 0x01:
+      target.title = value;
+      break;
+    case 0x02:
+      target.performer = value;
+      break;
+    case 0x03:
+      target.songwriter = value;
+      break;
+    case 0x04:
+      target.composer = value;
+      break;
+    case 0x05:
+      target.arranger = value;
+      break;
+    case 0x06:
+      target.message = value;
+      break;
+    case 0x07:
+      target.extraMessage = value;
+      break;
+    default:
+      break;
+  }
+}
+
+function firstNonEmptySacdText(values: string[]): string {
+  for (let index: number = 0; index < values.length; index++) {
+    const value: string = sanitizeSacdText(values[index]);
+    if (value.length > 0) {
+      return value;
+    }
+  }
+  return '';
+}
+
 function padTrackNumber(trackNumber: number): string {
   if (trackNumber < 10) {
     return `0${trackNumber}`;
@@ -914,6 +1205,7 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
   }
   const channelCount: number = entry.channelCount && entry.channelCount > 0 ? entry.channelCount : areaInfo.channelCount;
   const frameFormat: number = entry.frameFormat !== undefined ? entry.frameFormat : areaInfo.frameFormat;
+  const frameFilter: SacdTrackFrameFilter = createSacdTrackFrameFilter(entry);
   Logger.info('IsoArchiveTaskHelper', `heanup SACD area resolved channels=${channelCount}, frameFormat=${frameFormat}, areaFrameFormat=${areaInfo.frameFormat}`);
   let dstDecoderId: number | undefined = undefined;
   if (frameFormat === SACD_FRAME_FORMAT_DST) {
@@ -950,12 +1242,14 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
       started: false,
       size: 0,
       channelCount,
+      timecodeFrames: -1,
       dstEncoded: false,
       sectorCount: 0,
       buffer: new Uint8Array(SACD_MAX_FRAME_BUFFER_SIZE)
     };
+    const dstFrameBatch: SacdDstFrameBatch = createSacdDstFrameBatch();
 
-    const sectorsPerBatch: number = 128;
+    const sectorsPerBatch: number = 512;
     let sectorIndex: number = 0;
     while (sectorIndex < entry.lengthLsn) {
       const remainingSectors: number = entry.lengthLsn - sectorIndex;
@@ -978,6 +1272,8 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
           assembler,
           writer,
           dstDecoderId,
+          dstFrameBatch,
+          frameFilter,
           lastSector
         );
         if (!processed) {
@@ -993,6 +1289,18 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
         progressReporter);
     }
 
+    if (!flushSacdDstFrameBatch(dstFrameBatch, writer, dstDecoderId)) {
+      return {
+        success: false,
+        reason: '批量解码 SACD 音轨失败'
+      };
+    }
+    if (writer.sampleBytes <= 0) {
+      return {
+        success: false,
+        reason: 'SACD 音轨未提取到有效音频帧'
+      };
+    }
     finalizeDsfWriter(writer);
     Logger.info('IsoArchiveTaskHelper', `heanup SACD extract success path=${entry.path}, output=${outputPath}`);
     return { success: true };
@@ -1154,7 +1462,9 @@ function writeSacdFrameToDsf(writer: DsfWriterState, frameData: Uint8Array, chan
 }
 
 function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, assembler: SacdFrameAssembler,
-  writer: DsfWriterState, dstDecoderId: number | undefined, lastSector: boolean): boolean {
+  writer: DsfWriterState, dstDecoderId: number | undefined, dstFrameBatch: SacdDstFrameBatch,
+  frameFilter: SacdTrackFrameFilter,
+  lastSector: boolean): boolean {
   let offset: number = 0;
   const sectorHeader: number = sectorBuffer[offset];
   offset++;
@@ -1192,6 +1502,7 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
       }
       const extraByte: number = sectorBuffer[offset + 3];
       frameInfos.push({
+        timecodeFrames: readSacdTimecodeFrames(sectorBuffer, offset),
         sectorCount: (extraByte >> 2) & 0x1f,
         channelBit2: ((extraByte >> 1) & 0x01) === 1,
         channelBit3: (extraByte & 0x01) === 1
@@ -1199,6 +1510,7 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
       offset += 4;
     } else {
       frameInfos.push({
+        timecodeFrames: readSacdTimecodeFrames(sectorBuffer, offset),
         sectorCount: 0,
         channelBit2: false,
         channelBit3: false
@@ -1216,14 +1528,8 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
 
     if (packet.dataType === SACD_PACKET_TYPE_AUDIO) {
       if (packet.frameStart) {
-        if (assembler.started && assembler.size > 0) {
-          if (!isSacdFrameReady(assembler)) {
-            Logger.error('IsoArchiveTaskHelper',
-              `heanup SACD frameStart before previous frame ready: size=${assembler.size}, ` +
-                `dst=${assembler.dstEncoded}, remainingSectorCount=${assembler.sectorCount}`);
-            return false;
-          }
-          if (!finalizeSacdFrame(assembler, writer, dstDecoderId)) {
+        if (assembler.started && assembler.size > 0 && isSacdFrameReady(assembler)) {
+          if (!finalizeSacdFrame(assembler, writer, dstDecoderId, dstFrameBatch, frameFilter)) {
             return false;
           }
         }
@@ -1234,6 +1540,7 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
         assembler.dstEncoded = dstEncoded;
         assembler.sectorCount = frameInfo ? frameInfo.sectorCount : 0;
         assembler.channelCount = resolveSacdFrameChannelCount(frameInfo, areaChannelCount);
+        assembler.timecodeFrames = frameInfo ? frameInfo.timecodeFrames : -1;
         frameInfoIndex++;
       }
 
@@ -1242,12 +1549,6 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
           return false;
         }
         if (assembler.dstEncoded) {
-          if (assembler.sectorCount <= 0) {
-            Logger.error('IsoArchiveTaskHelper',
-              `heanup SACD DST packet overflow size=${assembler.size}, packetLength=${packet.packetLength}, ` +
-                `remainingSectorCount=${assembler.sectorCount}`);
-            return false;
-          }
           assembler.sectorCount--;
         }
       }
@@ -1261,19 +1562,17 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
       return true;
     }
     if (!isSacdFrameReady(assembler)) {
-      Logger.error('IsoArchiveTaskHelper',
-        `heanup SACD last sector frame incomplete size=${assembler.size}, dst=${assembler.dstEncoded}, ` +
-          `remainingSectorCount=${assembler.sectorCount}`);
       resetSacdFrameAssembler(assembler);
-      return false;
+      return true;
     }
-    return finalizeSacdFrame(assembler, writer, dstDecoderId);
+    return finalizeSacdFrame(assembler, writer, dstDecoderId, dstFrameBatch, frameFilter);
   }
   return true;
 }
 
 function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState,
-  dstDecoderId: number | undefined): boolean {
+  dstDecoderId: number | undefined, dstFrameBatch: SacdDstFrameBatch,
+  frameFilter: SacdTrackFrameFilter): boolean {
   if (!assembler.started || assembler.size <= 0) {
     resetSacdFrameAssembler(assembler);
     return true;
@@ -1285,26 +1584,33 @@ function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState
     resetSacdFrameAssembler(assembler);
     return false;
   }
-  let frameData: Uint8Array = assembler.buffer.subarray(0, assembler.size);
+  const frameData: Uint8Array = assembler.buffer.subarray(0, assembler.size);
+  if (!shouldWriteSacdFrame(frameFilter, assembler.timecodeFrames)) {
+    resetSacdFrameAssembler(assembler);
+    return true;
+  }
   if (assembler.dstEncoded) {
     if (!dstDecoderId || dstDecoderId <= 0) {
       resetSacdFrameAssembler(assembler);
       return false;
     }
-    try {
-      const dstFrameBuffer: ArrayBuffer = copySacdFrameToArrayBuffer(frameData);
-      const decodedBuffer: ArrayBuffer = sacdDstNativeBridge.decodeSacdDstFrame(dstDecoderId, dstFrameBuffer);
-      frameData = new Uint8Array(decodedBuffer);
-    } catch (error) {
-      Logger.error('IsoArchiveTaskHelper', `heanup DST decode throw: ${(error as Error).message}`);
+    if (!appendSacdDstFrameBatch(dstFrameBatch, frameData)) {
       resetSacdFrameAssembler(assembler);
       return false;
     }
-    if (frameData.length === 0 || frameData.length % assembler.channelCount !== 0) {
+    if (shouldFlushSacdDstFrameBatch(dstFrameBatch)) {
+      const flushed: boolean = flushSacdDstFrameBatch(dstFrameBatch, writer, dstDecoderId);
       resetSacdFrameAssembler(assembler);
-      return false;
+      return flushed;
     }
-  } else if (assembler.size % assembler.channelCount !== 0) {
+    resetSacdFrameAssembler(assembler);
+    return true;
+  }
+  if (!flushSacdDstFrameBatch(dstFrameBatch, writer, dstDecoderId)) {
+    resetSacdFrameAssembler(assembler);
+    return false;
+  }
+  if (assembler.size % assembler.channelCount !== 0) {
     resetSacdFrameAssembler(assembler);
     return false;
   }
@@ -1320,14 +1626,7 @@ function isSacdFrameReady(assembler: SacdFrameAssembler): boolean {
   if (assembler.dstEncoded) {
     return assembler.sectorCount === 0;
   }
-  return assembler.size % SACD_FRAME_SIZE_64 === 0;
-}
-
-function copySacdFrameToArrayBuffer(frameData: Uint8Array): ArrayBuffer {
-  const buffer: ArrayBuffer = new ArrayBuffer(frameData.length);
-  const copied: Uint8Array = new Uint8Array(buffer);
-  copied.set(frameData);
-  return buffer;
+  return assembler.size === assembler.channelCount * SACD_FRAME_SIZE_64;
 }
 
 function appendSacdFrameBytes(assembler: SacdFrameAssembler, source: Uint8Array): boolean {
@@ -1342,10 +1641,128 @@ function appendSacdFrameBytes(assembler: SacdFrameAssembler, source: Uint8Array)
 function resetSacdFrameAssembler(assembler: SacdFrameAssembler): void {
   assembler.started = false;
   assembler.size = 0;
+  assembler.timecodeFrames = -1;
   assembler.dstEncoded = false;
   assembler.sectorCount = 0;
 }
 
+function createSacdTrackFrameFilter(entry: IsoArchiveHelperAudioEntry): SacdTrackFrameFilter {
+  if ((entry.startTimeFrames === undefined || entry.startTimeFrames < 0) ||
+    !entry.durationFrames || entry.durationFrames <= 0) {
+    return {
+      enabled: false,
+      startFrame: 0,
+      endFrameExclusive: 0
+    };
+  }
+  return {
+    enabled: true,
+    startFrame: entry.startTimeFrames,
+    endFrameExclusive: entry.startTimeFrames + entry.durationFrames
+  };
+}
+
+function createSacdDstFrameBatch(): SacdDstFrameBatch {
+  return {
+    totalPayloadBytes: 0,
+    frameCount: 0,
+    frameLengths: [],
+    frameChunks: []
+  };
+}
+
+function appendSacdDstFrameBatch(batch: SacdDstFrameBatch, frameData: Uint8Array): boolean {
+  if (frameData.length <= 0) {
+    return false;
+  }
+  const copiedFrame: Uint8Array = new Uint8Array(frameData.length);
+  copiedFrame.set(frameData);
+  batch.frameChunks.push(copiedFrame);
+  batch.frameLengths.push(frameData.length);
+  batch.totalPayloadBytes += frameData.length;
+  batch.frameCount++;
+  return true;
+}
+
+function shouldFlushSacdDstFrameBatch(batch: SacdDstFrameBatch): boolean {
+  return batch.frameCount >= SACD_DST_BATCH_MAX_FRAMES || batch.totalPayloadBytes >= SACD_DST_BATCH_MAX_BYTES;
+}
+
+function flushSacdDstFrameBatch(batch: SacdDstFrameBatch, writer: DsfWriterState,
+  dstDecoderId: number | undefined): boolean {
+  if (batch.frameCount <= 0) {
+    return true;
+  }
+  if (!dstDecoderId || dstDecoderId <= 0) {
+    clearSacdDstFrameBatch(batch);
+    return false;
+  }
+
+  const payloadBuffer: ArrayBuffer = new ArrayBuffer(batch.totalPayloadBytes);
+  const payloadBytes: Uint8Array = new Uint8Array(payloadBuffer);
+  let payloadOffset: number = 0;
+  for (let index: number = 0; index < batch.frameChunks.length; index++) {
+    const frameChunk: Uint8Array = batch.frameChunks[index];
+    payloadBytes.set(frameChunk, payloadOffset);
+    payloadOffset += frameChunk.length;
+  }
+
+  const frameLengthsBuffer: ArrayBuffer = new ArrayBuffer(batch.frameCount * 4);
+  const frameLengthsArray: Uint32Array = new Uint32Array(frameLengthsBuffer);
+  for (let index: number = 0; index < batch.frameLengths.length; index++) {
+    frameLengthsArray[index] = batch.frameLengths[index];
+  }
+
+  try {
+    const decodedBuffer: ArrayBuffer = sacdDstNativeBridge.decodeSacdDstFrames(dstDecoderId, payloadBuffer,
+      frameLengthsBuffer);
+    const decodedBytes: Uint8Array = new Uint8Array(decodedBuffer);
+    const frameOutputSize: number = writer.channelCount * SACD_FRAME_SIZE_64;
+    if (decodedBytes.length !== frameOutputSize * batch.frameCount) {
+      clearSacdDstFrameBatch(batch);
+      return false;
+    }
+
+    let decodedOffset: number = 0;
+    for (let index: number = 0; index < batch.frameCount; index++) {
+      const frameSlice: Uint8Array = decodedBytes.subarray(decodedOffset, decodedOffset + frameOutputSize);
+      if (!writeSacdFrameToDsf(writer, frameSlice, writer.channelCount)) {
+        clearSacdDstFrameBatch(batch);
+        return false;
+      }
+      decodedOffset += frameOutputSize;
+    }
+  } catch (error) {
+    Logger.error('IsoArchiveTaskHelper', `heanup DST batch decode throw: ${(error as Error).message}`);
+    clearSacdDstFrameBatch(batch);
+    return false;
+  }
+
+  clearSacdDstFrameBatch(batch);
+  return true;
+}
+
+function clearSacdDstFrameBatch(batch: SacdDstFrameBatch): void {
+  batch.totalPayloadBytes = 0;
+  batch.frameCount = 0;
+  batch.frameLengths = [];
+  batch.frameChunks = [];
+}
+
+function shouldWriteSacdFrame(frameFilter: SacdTrackFrameFilter, timecodeFrames: number): boolean {
+  if (!frameFilter.enabled || timecodeFrames < 0) {
+    return true;
+  }
+  return timecodeFrames >= frameFilter.startFrame && timecodeFrames < frameFilter.endFrameExclusive;
+}
+
+function readSacdTimecodeFrames(buffer: Uint8Array, offset: number): number {
+  if (offset + 2 >= buffer.length) {
+    return -1;
+  }
+  return buffer[offset] * 60 * SACD_FRAME_RATE + buffer[offset + 1] * SACD_FRAME_RATE + buffer[offset + 2];
+}
+
 function resolveSacdFrameChannelCount(frameInfo: SacdFrameInfo | undefined, defaultChannelCount: number): number {
   if (!frameInfo) {
     return defaultChannelCount > 0 ? defaultChannelCount : 2;

+ 5 - 0
entry/src/main/ets/common/util/IsoArchiveUtil.ets

@@ -19,6 +19,11 @@ export interface IsoArchiveAudioEntry {
   areaType?: string;
   channelCount?: number;
   frameFormat?: number;
+  title?: string;
+  album?: string;
+  artist?: string;
+  albumArtist?: string;
+  comment?: string;
 }
 
 export interface IsoExtractTaskResult {

+ 122 - 27
entry/src/main/ets/common/util/RcpSocketUtil.ets

@@ -1,6 +1,6 @@
 // rcp通信工具
 import { BusinessError } from '@kit.BasicServicesKit';
-import { buffer, HashMap, JSON, util, xml } from '@kit.ArkTS';
+import { buffer, HashMap, JSON, taskpool, util, xml } from '@kit.ArkTS';
 import { FileInfo } from '../../viewmodel/FileInfo';
 import { rcp } from '@kit.RemoteCommunicationKit';
 import { BackgroundManager } from './BackgroundManager';
@@ -8,6 +8,104 @@ import FileManager, { merge2paths } from './FileManager';
 
 const UtilName = "heanup RcpSocket"
 
+@Concurrent
+async function parsePropFindResponseTask(xmlContent: string, rootpath: string): Promise<FileInfo[]> {
+  const safeDecodeUri = (value: string): string => {
+    try {
+      return decodeURI(value);
+    } catch (_error) {
+      return value;
+    }
+  };
+
+  const safeDecodeUriComponent = (value: string): string => {
+    try {
+      return decodeURIComponent(value);
+    } catch (_error) {
+      return value;
+    }
+  };
+
+  const decodeXmlEntitiesForTask = (value: string): string => {
+    let result = value
+      .replace(/&amp;/g, '&')
+      .replace(/&lt;/g, '<')
+      .replace(/&gt;/g, '>')
+      .replace(/&quot;/g, '"')
+      .replace(/&apos;/g, "'");
+    result = safeDecodeUriComponent(result);
+    return result;
+  };
+
+  const convertHttpDateToUnixTimestampForTask = (dateString: string): number => {
+    const date = new Date(dateString);
+    if (isNaN(date.getTime())) {
+      return 0;
+    }
+    return Math.floor(date.getTime() / 1000);
+  };
+
+  const filesInfo: FileInfo[] = [];
+  const responseRegex = /<D:response\b[^>]*>([\s\S]*?)<\/D:response>/gi;
+  let responseMatch: RegExpExecArray | null;
+
+  while ((responseMatch = responseRegex.exec(xmlContent)) !== null) {
+    const responseBlock = responseMatch[1];
+    const hrefMatch = responseBlock.match(/<D:href>(.*?)<\/D:href>/i);
+    if (!hrefMatch) {
+      continue;
+    }
+
+    const fullHref = decodeXmlEntitiesForTask(safeDecodeUri(hrefMatch[1]));
+
+    let displayNameMatch = responseBlock.match(/<D:displayname>(.*?)<\/D:displayname>/i);
+    if (!displayNameMatch) {
+      displayNameMatch = responseBlock.match(/<lp1:displayname>(.*?)<\/lp1:displayname>/i);
+    }
+
+    let name = '';
+    if (displayNameMatch && displayNameMatch[1]) {
+      name = decodeXmlEntitiesForTask(displayNameMatch[1]);
+    } else {
+      name = fullHref;
+      const lastSlashIndex = fullHref.lastIndexOf('/');
+      if (lastSlashIndex >= 0 && lastSlashIndex < fullHref.length - 1) {
+        name = fullHref.substring(lastSlashIndex + 1);
+      } else if (fullHref.endsWith('/')) {
+        const withoutTrailingSlash = fullHref.substring(0, fullHref.length - 1);
+        const secondLastSlash = withoutTrailingSlash.lastIndexOf('/');
+        if (secondLastSlash >= 0) {
+          name = withoutTrailingSlash.substring(secondLastSlash + 1) + '/';
+        }
+      }
+    }
+
+    if (name === '' || name === '/') {
+      continue;
+    }
+
+    let sizeMatch = responseBlock.match(/<lp1:getcontentlength>(.*?)<\/lp1:getcontentlength>/i);
+    if (!sizeMatch) {
+      sizeMatch = responseBlock.match(/<D:getcontentlength>(.*?)<\/D:getcontentlength>/i);
+    }
+
+    let lastModifiedMatch = responseBlock.match(/<D:getlastmodified>(.*?)<\/D:getlastmodified>/i);
+    if (!lastModifiedMatch) {
+      lastModifiedMatch = responseBlock.match(/<lp1:getlastmodified>(.*?)<\/lp1:getlastmodified>/i);
+    }
+    const lastModified = lastModifiedMatch ? convertHttpDateToUnixTimestampForTask(lastModifiedMatch[1]) : 0;
+    const size = sizeMatch ? Number(sizeMatch[1]) : 0;
+
+    const fileInfo = new FileInfo(rootpath, name, size, lastModified);
+    fileInfo.href = fullHref;
+    fileInfo.contentLength = size;
+    fileInfo.isDirectory = fullHref.endsWith('/') || size === 0;
+    filesInfo.push(fileInfo);
+  }
+
+  return filesInfo;
+}
+
 export class RcpSocket {
   private static instance: RcpSocket;
   public ErrorMessage: string | BusinessError = ''
@@ -328,9 +426,9 @@ export class RcpSocket {
       // 创建 RCP 会话配置
       let response: string = ''
       const customHttpEventsHandler: rcp.HttpEventsHandler = {
-        onDataReceive: async (incomingData: ArrayBuffer) => {
+        onDataReceive: (incomingData: ArrayBuffer) => {
           response += this.buf2String(incomingData)
-          await this.backgroundManager.updateDataTransferContinuousTask()
+          void this.backgroundManager.updateDataTransferContinuousTask()
         },
         onDataEnd: () => {
         },
@@ -400,34 +498,28 @@ export class RcpSocket {
       // 发起请求
       try {
         await rcpSession.fetch(req)
-          .finally(() => {
-            if (response != '') {
-              // 提取文件信息
-              const filesInfo = this.extractHrefContents(response, path, url);
-              if (filesInfo.length !== 0) {
-                rcpSession.close()
-                resolve(filesInfo);
-              } else {
-                let message = `请求${host}失败,响应信息:${response}`
-                console.error(UtilName, 'testTag', message)
-                rcpSession.close()
-                reject(message)
-              }
-            } else {
-              let error = `服务器响应信息为空,请求失败`
-              console.error(UtilName, 'testTag', error)
-              rcpSession.close()
-              reject(error);
-            }
-          })
-        // 处理成功响应
-        //console.info(UtilName, 'testTag', JSON.stringify(res));
+        if (response === '') {
+          const error = `服务器响应信息为空,请求失败`
+          console.error(UtilName, 'testTag', error)
+          reject(error);
+          return
+        }
+        const parseTask = new taskpool.Task(parsePropFindResponseTask, response, path)
+        const filesInfo = await taskpool.execute(parseTask, taskpool.Priority.MEDIUM) as FileInfo[]
+        if (filesInfo.length !== 0) {
+          resolve(filesInfo);
+          return
+        }
+        const message = `请求${host}失败,响应信息:${response}`
+        console.error(UtilName, 'testTag', message)
+        reject(message)
       } catch (err) {
         // 处理错误响应
         console.error(UtilName, "testTag", `请求${url}失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
         let error = `错误码${err.code},错误信息${err.data}`
-        rcpSession.close()
         reject(error)
+      } finally {
+        rcpSession.close()
       }
     });
   }
@@ -1042,7 +1134,10 @@ export class RcpSocket {
 
         // 发起上传请求
         console.info(UtilName, 'testTag', '发起HTTP PUT请求...');
-        await rcpSession.fetch(req);
+        const response = await rcpSession.fetch(req);
+        if (response.statusCode < 200 || response.statusCode >= 300) {
+          throw new Error(`HTTP ${response.statusCode}`);
+        }
         
         console.info(UtilName, 'testTag', '上传请求完成,关闭会话');
         if (rcpSession) {

+ 54 - 0
entry/src/main/ets/common/util/SearchHistoryUtil.ets

@@ -0,0 +1,54 @@
+import { PreferencesUtil } from '@pura/harmony-utils';
+
+export class SearchHistoryUtil {
+  private static readonly LIMIT: number = 20;
+  private static readonly PREFIX: string = 'search_history_';
+
+  static load(scope: string): string[] {
+    if (!scope || scope.length === 0) {
+      return [];
+    }
+    const raw = PreferencesUtil.getStringSync(`${SearchHistoryUtil.PREFIX}${scope}`, '');
+    if (!raw || raw.length === 0) {
+      return [];
+    }
+    try {
+      const parsed = JSON.parse(raw) as Array<string | number | boolean | object | null>;
+      if (!Array.isArray(parsed)) {
+        return [];
+      }
+      const result: string[] = [];
+      for (let i = 0; i < parsed.length; i++) {
+        const item: string | number | boolean | object | null = parsed[i];
+        if (typeof item === 'string') {
+          const keyword = item.trim();
+          if (keyword.length > 0) {
+            result.push(keyword);
+          }
+        }
+      }
+      return result.slice(0, SearchHistoryUtil.LIMIT);
+    } catch (_error) {
+      return [];
+    }
+  }
+
+  static save(scope: string, keyword: string): string[] {
+    const normalized = keyword.trim();
+    if (!scope || scope.length === 0 || normalized.length === 0) {
+      return SearchHistoryUtil.load(scope);
+    }
+    const current = SearchHistoryUtil.load(scope);
+    const next = [normalized, ...current.filter((item: string) => item !== normalized)]
+      .slice(0, SearchHistoryUtil.LIMIT);
+    PreferencesUtil.putSync(`${SearchHistoryUtil.PREFIX}${scope}`, JSON.stringify(next));
+    return next;
+  }
+
+  static clear(scope: string): void {
+    if (!scope || scope.length === 0) {
+      return;
+    }
+    PreferencesUtil.putSync(`${SearchHistoryUtil.PREFIX}${scope}`, '');
+  }
+}

+ 66 - 53
entry/src/main/ets/pages/PlaylistDetailPage.ets

@@ -13,6 +13,7 @@ import { common } from '@kit.AbilityKit';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { ConfigurationConstant } from '@kit.AbilityKit';
 import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
+import { PlayingIndicator } from '../view/PlayingIndicator';
 
 // 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
 function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
@@ -719,30 +720,36 @@ export struct PlaylistDetailPage {
           .justifyContent(FlexAlign.Center)
 
           // 排序模式下显示上下移动按钮,否则显示更多按钮
-          if (this.isSortMode) {
-            Row({ space: 8 }) {
-              // 上移按钮
-              Text('▲')
-                .fontSize(16)
-                .fontColor(index === 0 ? (this.isDarkMode ? '#666666' : '#cccccc') :
-                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
-                .enabled(index > 0)
+          Row({ space: 10 }) {
+            PlayingIndicator({
+              isActive: this.isPlaying && this.curIndex === index,
+              indicatorSize: 18
+            })
 
-              // 下移按钮
-              Text('▼')
-                .fontSize(16)
-                .fontColor(index === this.songList.length - 1 ? (this.isDarkMode ? '#666666' : '#cccccc') :
-                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
-                .enabled(index < this.songList.length - 1)
+            if (this.isSortMode) {
+              Row({ space: 8 }) {
+                // 上移按钮
+                Text('▲')
+                  .fontSize(16)
+                  .fontColor(index === 0 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                    (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                  .enabled(index > 0)
+
+                // 下移按钮
+                Text('▼')
+                  .fontSize(16)
+                  .fontColor(index === this.songList.length - 1 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                    (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                  .enabled(index < this.songList.length - 1)
+              }
+            } else {
+              Text('⋯')
+                .fontSize(20)
+                .fontColor($r('app.color.text_color'))
+                .opacity(0.6)
             }
-            .margin({ right: 15 })
-          } else {
-            Text('⋯')
-              .fontSize(20)
-              .fontColor($r('app.color.text_color'))
-              .opacity(0.6)
-              .margin({ right: 15 })
           }
+          .margin({ right: 15 })
         }
         .width('100%')
         .height(70)
@@ -832,41 +839,47 @@ export struct PlaylistDetailPage {
           .justifyContent(FlexAlign.Center)
 
           // 排序模式下显示上下移动按钮,否则显示更多按钮
-          if (this.isSortMode) {
-            Row({ space: 8 }) {
-              // 上移按钮
-              Text('▲')
-                .fontSize(16)
-                .fontColor(index === 0 ? (this.isDarkMode ? '#666666' : '#cccccc') :
-                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
-                .onClick(() => {
-                  if (index > 0) {
-                    this.moveSong(index, index - 1)
-                  }
-                })
-                .enabled(index > 0)
+          Row({ space: 10 }) {
+            PlayingIndicator({
+              isActive: this.isPlaying && this.curIndex === index,
+              indicatorSize: 18
+            })
 
-              // 下移按钮
-              Text('▼')
-                .fontSize(16)
-                .fontColor(index === this.songList.length - 1 ? (this.isDarkMode ? '#666666' : '#cccccc') :
-                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
-                .onClick(() => {
-                  if (index < this.songList.length - 1) {
-                    this.moveSong(index, index + 1)
-                  }
-                })
-                .enabled(index < this.songList.length - 1)
+            if (this.isSortMode) {
+              Row({ space: 8 }) {
+                // 上移按钮
+                Text('▲')
+                  .fontSize(16)
+                  .fontColor(index === 0 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                    (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                  .onClick(() => {
+                    if (index > 0) {
+                      this.moveSong(index, index - 1)
+                    }
+                  })
+                  .enabled(index > 0)
+
+                // 下移按钮
+                Text('▼')
+                  .fontSize(16)
+                  .fontColor(index === this.songList.length - 1 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                    (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                  .onClick(() => {
+                    if (index < this.songList.length - 1) {
+                      this.moveSong(index, index + 1)
+                    }
+                  })
+                  .enabled(index < this.songList.length - 1)
+              }
+            } else {
+              Text('⋯')
+                .fontSize(20)
+                .fontColor($r('app.color.text_color'))
+                .opacity(0.6)
+                .bindMenu(this.MoreMenuBuilder(song))
             }
-            .margin({ right: 15 })
-          } else {
-            Text('⋯')
-              .fontSize(20)
-              .fontColor($r('app.color.text_color'))
-              .opacity(0.6)
-              .margin({ right: 15 })
-              .bindMenu(this.MoreMenuBuilder(song))
           }
+          .margin({ right: 15 })
         }
         .width('100%')
         .height(70)

+ 5 - 0
entry/src/main/ets/pages/SettingPage.ets

@@ -34,6 +34,7 @@ import { audio } from '@kit.AudioKit'
 // @Entry
 @Component
 export struct SettingPage {
+  @State default_layout:number = 0
   @State showFindLocation: boolean = true
   @State showZMIndex: boolean = false //是否右侧显示字母索引
   @State autoHideTitle: boolean = true //滚动自动隐藏标题栏
@@ -272,6 +273,7 @@ export struct SettingPage {
 
   // 组件生命周期
   aboutToAppear() {
+    this.default_layout =  PreferencesUtil.getNumberSync('default_layout', 0)
     this.defalut_home_type = PreferencesUtil.getNumberSync('defalut_home_type', 0)
     this.showFindLocation  = PreferencesUtil.getBooleanSync('showFindLocation', true)
     this.showZMIndex = PreferencesUtil.getBooleanSync('showZMIndex', false)
@@ -1090,8 +1092,11 @@ export struct SettingPage {
                 .value(CommonConstants.DEFAULT_LAYOUT[this.twoFingerType==4?0:
                   this.isGridMusic?2:1])
                 .onSelect(async (_index: number, text?: string | undefined) => {
+                  this.default_layout = _index
+                  PreferencesUtil.put('default_layout',this.default_layout)
                   if(_index == 0){
                     PreferencesUtil.put(SettingPage.TWO_FINGER_TYPE,4)
+
                   }else{
                     if(this.twoFingerType!=4){
                       PreferencesUtil.put(SettingPage.TWO_FINGER_TYPE,this.twoFingerType)

+ 23 - 13
entry/src/main/ets/pages/UploadMusicPage.ets

@@ -350,6 +350,9 @@ export struct UploadMusicPage {
       case RemoteDriveManagerStates.UploadSuccess:
         this.handleUploadSuccess();
         break;
+      case RemoteDriveManagerStates.UploadSkipped:
+        this.handleUploadSkipped();
+        break;
       case RemoteDriveManagerStates.UploadFailed:
         this.handleUploadFailed();
         break;
@@ -427,6 +430,17 @@ export struct UploadMusicPage {
     this.onResult(true)
   }
 
+  /**
+   * 处理上传跳过事件
+   */
+  private handleUploadSkipped(): void {
+    Logger.warn(TAG, '上传被跳过');
+    promptAction.showToast({
+      message: '远程已存在同名文件,已跳过上传',
+      duration: 2000
+    });
+  }
+
   /**
    * 处理上传失败事件
    */
@@ -924,21 +938,17 @@ export struct UploadMusicPage {
       this.isLoadingFolders = true;
       Logger.info(TAG, `加载文件夹列表: ${path}`);
 
-      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, path);
-
-      // 过滤出文件夹
-      const allFiles = this.webdavManager.webDavFiles;
-      this.browseFolders = [];
+      const snapshot = await this.webdavManager.loadDirectorySnapshotSilently(this.selectedAccount, path);
+      const nextFolders: FileInfo[] = [];
       const normalizedCurrent = this.normalizeBrowsePath(this.currentBrowsePath);
-      for (let i = 0; i < allFiles.length; i++) {
-        const file = allFiles[i];
-        if (file.isDirectory) {
-          const folderPath = this.normalizeBrowsePath(file.href);
-          if (folderPath !== normalizedCurrent) {
-            this.browseFolders.push(file);
-          }
+      for (let i = 0; i < snapshot.folders.length; i++) {
+        const file = snapshot.folders[i];
+        const folderPath = this.normalizeBrowsePath(file.href);
+        if (folderPath !== normalizedCurrent) {
+          nextFolders.push(file);
         }
       }
+      this.browseFolders = nextFolders;
 
       Logger.info(TAG, `加载了 ${this.browseFolders.length} 个文件夹`);
     } catch (error) {
@@ -2381,4 +2391,4 @@ export struct UploadMusicPage {
       .margin({bottom:12})
     }
   }
-}
+}

+ 8 - 12
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -35,6 +35,7 @@ import { DownloadCenterManager, DownloadCenterTask } from '../common/util/Downlo
 import { DownloadCenter } from '../view/DownloadCenter';
 import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
+import { PlayingIndicator } from '../view/PlayingIndicator';
 
 /**
  * 歌单播放事件数据
@@ -3912,18 +3913,13 @@ export struct WebDavMainPage {
             .width(22)
             .height(22)
 
-          ImageAnimator()
-            .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组
-            .duration(1000)// 持续
-            //.state(this.animationState)// 动画状态
-            .state(AnimationStatus.Running)// 动画状态
-            .fillMode(FillMode.Forwards)
-            .width(18)
-            .margin({ right: 12, top: 8, bottom: 8 })
-            .visibility(this.currentSong?.filePath==song.filePath ? (this.isMultiSelect ? Visibility.None : Visibility.Visible) :
-              Visibility.None)
-            .height(18)
-            .iterations(-1) // 播放次数
+          PlayingIndicator({
+            isActive: this.currentSong?.filePath == song.filePath && !this.isMultiSelect,
+            indicatorSize: 18,
+            marginRight: 12,
+            marginTop: 8,
+            marginBottom: 8
+          })
         }
       }
     }

+ 118 - 29
entry/src/main/ets/view/IsoExtractComptent.ets

@@ -9,12 +9,19 @@ export struct IsoExtractComptent {
   @Prop entries: IsoArchiveAudioEntry[] = []
   @Prop selectedPaths: string[] = []
   @Prop isExtracting: boolean = false
+  @Prop extractProgress: number = 0
+  @Prop extractProgressText: string = ''
+  @Prop extractCurrentName: string = ''
   onToggleSelection = (path: string, checked: boolean) => {
   }
   onToggleSelectAll = () => {
   }
   onExtract = () => {
   }
+  onRunInBackground = () => {
+  }
+  onStopExtract = () => {
+  }
   onCancel = () => {
   }
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
@@ -23,6 +30,15 @@ export struct IsoExtractComptent {
     return this.selectedPaths.includes(path)
   }
 
+  private getEntryMetaText(item: IsoArchiveAudioEntry): string {
+    const sizeText = Utility.formatFSize(item.size)
+    if (item.entryType === 'sacd_track') {
+      const areaText = item.areaType && item.areaType.length > 0 ? ` ${item.areaType}` : ''
+      return `SACD 音轨${areaText} · ${sizeText}`
+    }
+    return Utility.isVideoByExtension(item.path) ? `视频 · ${sizeText}` : `音频 · ${sizeText}`
+  }
+
   build() {
     Column() {
       Row() {
@@ -33,7 +49,7 @@ export struct IsoExtractComptent {
             .maxLines(1)
             .textOverflow({ overflow: this.isLongNameRoLL ? TextOverflow.MARQUEE : TextOverflow.Ellipsis })
 
-          Text(`ISO 音频 ${this.entries.length} 项,已选 ${this.selectedPaths.length} 项`)
+          Text(`ISO 文件 ${this.entries.length} 项,已选 ${this.selectedPaths.length} 项`)
             .fontSize(12)
             .fontColor(Color.Gray)
         }
@@ -52,20 +68,69 @@ export struct IsoExtractComptent {
         .margin({ right: 18 })
         .backgroundColor(Color.Transparent)
         .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
-        .enabled(!this.isExtracting)
         .onClick(() => {
           this.onCancel()
         })
       }
       .margin({ top: 20, bottom: 8 })
 
+      if (this.isExtracting) {
+        Column({ space: 10 }) {
+          Row({ space: 8 }) {
+            SymbolGlyph($r('sys.symbol.arrow_down_circle'))
+              .fontSize(18)
+              .fontColor([this.themeColor])
+
+            Text(`实时提取进度 ${this.extractProgress}%`)
+              .fontSize(14)
+              .fontWeight(FontWeight.Medium)
+              .fontColor($r('app.color.text_color'))
+          }
+          .width('100%')
+
+          Progress({ value: this.extractProgress, total: 100, type: ProgressType.Linear })
+            .width('100%')
+            .color(this.themeColor)
+            .backgroundColor($r('app.color.track_color'))
+            .style({ strokeWidth: 6 })
+
+          Text(this.extractProgressText.length > 0 ? this.extractProgressText : '准备提取')
+            .fontSize(12)
+            .fontColor(Color.Gray)
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+          if (this.extractCurrentName.length > 0) {
+            Text(this.extractCurrentName)
+              .fontSize(12)
+              .fontColor($r('app.color.text_color'))
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+          }
+
+          Text('关闭面板后会继续在后台提取,你可以继续执行其他操作')
+            .fontSize(11)
+            .fontColor(Color.Gray)
+        }
+        .width('100%')
+        .alignItems(HorizontalAlign.Start)
+        .padding({ left: 18, right: 18, top: 14, bottom: 14 })
+        .margin({ left: 25, right: 25, bottom: 10 })
+        .backgroundColor($r('app.color.start_window_background'))
+        .border({
+          width: { bottom: 1 },
+          color: '#E9E9E9'
+        })
+        .borderRadius(18)
+      }
+
       if (this.entries.length === 0) {
         Column({ space: 8 }) {
-          SymbolGlyph($r('sys.symbol.music_note_list'))
+          SymbolGlyph($r('sys.symbol.doc_text'))
             .fontSize(34)
             .fontColor([Color.Gray])
 
-          Text('ISO 内未发现可提取的音频文件')
+          Text('ISO 内未发现可提取的媒体文件')
             .fontSize(14)
             .fontColor(Color.Gray)
         }
@@ -96,7 +161,7 @@ export struct IsoExtractComptent {
                     .textOverflow({ overflow: this.isLongNameRoLL ? TextOverflow.MARQUEE : TextOverflow.Ellipsis })
                     .fontColor($r('app.color.text_color'))
 
-                  Text(Utility.formatFSize(item.size))
+                  Text(this.getEntryMetaText(item))
                     .fontSize(11)
                     .fontColor(Color.Gray)
                 }
@@ -119,30 +184,54 @@ export struct IsoExtractComptent {
         })
       }
 
-      Row() {
-        Button(this.selectedPaths.length === this.entries.length && this.entries.length > 0 ? '取消全选' : '全选')
-          .layoutWeight(1)
-          .height(44)
-          .borderRadius(22)
-          .fontSize(14)
-          .fontColor(this.themeColor)
-          .backgroundColor('#1A000000')
-          .enabled(!this.isExtracting && this.entries.length > 0)
-          .onClick(() => {
-            this.onToggleSelectAll()
-          })
-
-        Button(this.isExtracting ? '提取中...' : '提取文件')
-          .layoutWeight(1)
-          .height(44)
-          .borderRadius(22)
-          .fontSize(14)
-          .fontColor(Color.White)
-          .backgroundColor(this.themeColor)
-          .enabled(!this.isExtracting && this.selectedPaths.length > 0)
-          .onClick(() => {
-            this.onExtract()
-          })
+      Row({space:15}) {
+        if (this.isExtracting) {
+          Button('后台运行')
+            .layoutWeight(1)
+            .height(44)
+            .borderRadius(22)
+            .fontSize(14)
+            .fontColor(this.themeColor)
+            .backgroundColor('#1A000000')
+            .onClick(() => {
+              this.onRunInBackground()
+            })
+
+          Button('停止提取')
+            .layoutWeight(1)
+            .height(44)
+            .borderRadius(22)
+            .fontSize(14)
+            .fontColor(Color.White)
+            .backgroundColor($r('app.color.btn_red'))
+            .onClick(() => {
+              this.onStopExtract()
+            })
+        } else {
+          Button(this.selectedPaths.length === this.entries.length && this.entries.length > 0 ? '取消全选' : '全选')
+            .layoutWeight(1)
+            .height(44)
+            .borderRadius(22)
+            .fontSize(14)
+            .fontColor(this.themeColor)
+            .backgroundColor('#1A000000')
+            .enabled(this.entries.length > 0)
+            .onClick(() => {
+              this.onToggleSelectAll()
+            })
+
+          Button('提取文件')
+            .layoutWeight(1)
+            .height(44)
+            .borderRadius(22)
+            .fontSize(14)
+            .fontColor(Color.White)
+            .backgroundColor(this.themeColor)
+            .enabled(this.selectedPaths.length > 0)
+            .onClick(() => {
+              this.onExtract()
+            })
+        }
       }
       .padding({ left: 24, right: 24, top: 12, bottom: 20 })
       .margin({ left: 6, right: 6 })

+ 295 - 123
entry/src/main/ets/view/LocalMusic.ets

@@ -65,6 +65,7 @@ import { Verify } from './Verify';
 import { taskpool } from '@kit.ArkTS';
 import { RotatingCover } from './RotatingCover';
 import { SpectrumView } from './spectrum/SpectrumView';
+import { PlayingIndicator } from './PlayingIndicator';
 import { PointLightButton } from './PointLight/PointLightButton';
 import { PlayConstants } from '../common/constants/PlayConstants';
 import { effectKit } from '@kit.ArkGraphics2D';
@@ -201,7 +202,7 @@ import {
 
 const TAG = 'LocalMusic';
 const HICAR_LOG_TAG = 'HiCarWindow';
-const PREF_PLAY_COVER_SCALE_GUIDE_SHOWN = 'play_cover_scale_guide_shown';
+const PREF_PLAY_COVER_SCALE_GUIDE_SHOWN = 'play_cover_scale_guide_show';
 
 function logHiCarWindow(message: string): void {
   void ServerLogUtil.info(HICAR_LOG_TAG, message);
@@ -377,6 +378,7 @@ export enum SortMode {
 @Preview
 @Component
 export struct LocalMusic {
+  @State default_layout:number = 0
   @State isGeKongPlayNext: boolean = true//是否开启隔空手势播放下一首
   @State bluetoothDisconnectPause: boolean = false // 蓝牙设备断开暂停
   @State isBluetoothConnected: boolean = false // 蓝牙连接状态
@@ -436,7 +438,11 @@ export struct LocalMusic {
   @State selectedIsoAudioPaths: string[] = []
   @State isoSourceFileName: string = ''
   @State isIsoExtracting: boolean = false
+  @State isoExtractProgress: number = 0
+  @State isoExtractProgressText: string = ''
+  @State isoExtractCurrentEntryName: string = ''
   private isoSourceFilePath: string = ''
+  private isoExtractDestinationDir: string = ''
   private isoComponentId: number = 0
   private isoExtractProgressTimer: number = 0
   private isoExtractExpectedOutputPaths: Array<string> = []
@@ -1410,6 +1416,7 @@ export struct LocalMusic {
     context.getApplicationContext().setColorMode(colorMode)
   }
   initSetting() {
+    this.default_layout =  PreferencesUtil.getNumberSync('default_layout', 0)
     this.isGeKongPlayNext  = PreferencesUtil.getBooleanSync('isGeKongPlayNext', false)
     this.bluetoothDisconnectPause = PreferencesUtil.getBooleanSync(SettingPage.BLUETOOTH_DISCONNECT_PAUSE, false)
     this.bluetoothLyricEnabled = PreferencesUtil.getBooleanSync(SettingPage.BLUETOOTH_LYRIC_ENABLED, false)
@@ -5868,17 +5875,13 @@ export struct LocalMusic {
             .height(22)
 
 
-          ImageAnimator()
-            .images(this.imagesF)// 动画数组
-            .duration(1000)// 持续
-            .state(this.animationState)// 动画状态
-            .fillMode(FillMode.Forwards)
-            .width(18)
-            .margin({ right: 20, top: 8, bottom: 8 })
-            .visibility(this.videoUrl === item.filePath ? this.isMultiSelect ? Visibility.None : Visibility.Visible :
-              Visibility.None)
-            .height(18)
-            .iterations(-1) // 播放次数
+          PlayingIndicator({
+            isActive: this.videoUrl === item.filePath && !this.isMultiSelect,
+            indicatorSize: 18,
+            marginRight: 20,
+            marginTop: 8,
+            marginBottom: 8
+          })
         }
 
       }
@@ -6946,7 +6949,7 @@ export struct LocalMusic {
 
             MenuItem({
               symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')),
-              content: '提取ISO音频'
+              content: '提取ISO文件'
             })
               .visibility(this.isIsoLocalFile(item) ? Visibility.Visible : Visibility.None)
               .onClick(async () => {
@@ -7914,6 +7917,18 @@ export struct LocalMusic {
       return
     }
 
+    if (this.isIsoExtracting) {
+      if (ArrayUtil.isNotEmpty(this.isoAudioEntries)) {
+        if (this.isoSourceFilePath !== item.filePath) {
+          ToastUtil.showToast('当前有 ISO 提取任务正在后台执行,已打开提取进度窗口')
+        }
+        this.presentIsoArchiveDialog()
+        return
+      }
+      ToastUtil.showToast('当前有 ISO 提取任务正在后台执行')
+      return
+    }
+
     Logger.info(TAG, `heanup ISO open dialog path=${item.filePath}, name=${item.fileName || item.name}`)
     this.closeIsoDialog()
     this.isoSourceFilePath = item.filePath
@@ -7933,7 +7948,7 @@ export struct LocalMusic {
       Logger.info(TAG, `heanup ISO list entries done count=${this.isoAudioEntries.length}, path=${item.filePath}`)
       if (ArrayUtil.isEmpty(this.isoAudioEntries)) {
         Logger.error(TAG, `heanup ISO list entries empty path=${item.filePath}`)
-        ToastUtil.showToast('ISO 内未找到音频文件')
+        ToastUtil.showToast('ISO 内未发现可提取的媒体文件')
         this.closeIsoDialog()
         return
       }
@@ -7950,6 +7965,13 @@ export struct LocalMusic {
       }
     }
 
+    this.presentIsoArchiveDialog()
+  }
+
+  private presentIsoArchiveDialog(): void {
+    if (this.isoComponentId > 0) {
+      return
+    }
     this.getUIContext().getPromptAction().openCustomDialog({
       builder: () => {
         this.isoComptentBuilder()
@@ -7993,9 +8015,9 @@ export struct LocalMusic {
     Logger.info(TAG, `heanup ISO toggle select all count=${this.selectedIsoAudioPaths.length}`)
   }
 
-  private startIsoExtractProgressWatcher(entries: Array<IsoArchiveAudioEntry>): void {
+  private startIsoExtractProgressWatcher(entries: Array<IsoArchiveAudioEntry>, destinationDir: string): void {
     this.stopIsoExtractProgressWatcher()
-    this.isoExtractExpectedOutputPaths = this.buildIsoExtractExpectedOutputPaths(entries)
+    this.isoExtractExpectedOutputPaths = this.buildIsoExtractExpectedOutputPaths(entries, destinationDir)
     this.isoExtractTotalBytes = entries.reduce((total: number, entry: IsoArchiveAudioEntry) => {
       const size = entry && entry.size > 0 ? entry.size : 0
       return total + size
@@ -8027,8 +8049,8 @@ export struct LocalMusic {
   }
 
   private updateIsoExtractProgressFromFiles(): void {
-    if (!this.loadingProgressDialogId || this.isoExtractTotalBytes <= 0 ||
-      ArrayUtil.isEmpty(this.isoExtractExpectedOutputPaths) || this.isoExtractReceivedTaskProgress) {
+    if (this.isoExtractTotalBytes <= 0 || ArrayUtil.isEmpty(this.isoExtractExpectedOutputPaths) ||
+      this.isoExtractReceivedTaskProgress) {
       return
     }
 
@@ -8062,7 +8084,7 @@ export struct LocalMusic {
     }
 
     if (progress > 0) {
-      this.updateIsoExtractProgressDialog(progress,
+      this.updateIsoExtractProgressState(progress,
         `正在提取 ${progress}% (${existingCount}/${this.isoExtractExpectedOutputPaths.length})`)
       return
     }
@@ -8071,14 +8093,12 @@ export struct LocalMusic {
       Date.now() - this.isoExtractLastActivityTime > this.isoExtractStallTimeoutMs) {
       Logger.warn(TAG, `heanup ISO 提取进度长时间无变化: timeout=${this.isoExtractStallTimeoutMs}`)
       this.markIsoExtractActivity()
-      this.updateIsoExtractProgressDialog(1, '准备提取 1%')
+      this.updateIsoExtractProgressState(1, '准备提取 1%')
     }
   }
 
-  private updateIsoExtractProgressDialog(progress: number, message: string, force: boolean = false): void {
-    if (!this.loadingProgressDialogId) {
-      return
-    }
+  private updateIsoExtractProgressState(progress: number, message: string, currentName: string = '',
+    force: boolean = false): void {
     let safeProgress = progress
     if (safeProgress < 0) {
       safeProgress = 0
@@ -8090,28 +8110,27 @@ export struct LocalMusic {
       return
     }
     this.isoExtractDisplayedProgress = safeProgress
-    DialogHelper.updateLoading(this.loadingProgressDialogId, message, safeProgress)
+    this.isoExtractProgress = safeProgress
+    this.isoExtractProgressText = message
+    if (force || StrUtil.isNotEmpty(currentName)) {
+      this.isoExtractCurrentEntryName = currentName
+    }
   }
 
-  private closeIsoExtractLoadingDialog(): void {
-    if (!this.loadingProgressDialogId) {
-      return
-    }
-    try {
-      DialogHelper.closeDialog(this.loadingProgressDialogId)
-    } catch (_error) {
-    }
-    this.loadingProgressDialogId = ''
+  private resetIsoExtractProgressState(): void {
+    this.isoExtractProgress = 0
+    this.isoExtractProgressText = ''
+    this.isoExtractCurrentEntryName = ''
     this.isoExtractDisplayedProgress = 0
   }
 
-  private buildIsoExtractExpectedOutputPaths(entries: Array<IsoArchiveAudioEntry>): Array<string> {
+  private buildIsoExtractExpectedOutputPaths(entries: Array<IsoArchiveAudioEntry>, destinationDir: string): Array<string> {
     const outputPaths: Array<string> = []
     const occupiedPaths: Set<string> = new Set<string>()
     for (let i = 0; i < entries.length; i++) {
       const entry = entries[i]
       const fileName = entry.name || this.getIsoLastPathSegment(entry.path)
-      const outputPath = this.buildIsoExpectedOutputPath(this.currentPath, fileName, occupiedPaths)
+      const outputPath = this.buildIsoExpectedOutputPath(destinationDir, fileName, occupiedPaths)
       occupiedPaths.add(outputPath)
       outputPaths.push(outputPath)
     }
@@ -8174,14 +8193,14 @@ export struct LocalMusic {
     return path
   }
 
-  private cancelIsoExtraction(reason?: string, closeLoadingDialog: boolean = true): void {
+  private cancelIsoExtraction(reason?: string): void {
     const currentTask = this.isoExtractTask
     Logger.info(TAG,
-      `heanup ISO cancel request reason=${reason || ''}, hasTask=${currentTask ? 'true' : 'false'}, ` +
-        `closeLoading=${closeLoadingDialog}`)
+      `heanup ISO cancel request reason=${reason || ''}, hasTask=${currentTask ? 'true' : 'false'}`)
     this.isoExtractCanceled = true
     this.isoExtractCancelMessage = reason ? reason : ''
     this.isoExtractTask = undefined
+    this.updateIsoExtractProgressState(this.isoExtractProgress, '正在停止提取...', this.isoExtractCurrentEntryName, true)
     if (currentTask) {
       try {
         taskpool.terminateTask(currentTask)
@@ -8189,10 +8208,11 @@ export struct LocalMusic {
         Logger.error(TAG, `heanup 终止 ISO 提取任务失败: ${(error as Error).message}`)
       }
     }
-    if (closeLoadingDialog) {
-      this.closeIsoExtractLoadingDialog()
-    }
-    this.closeIsoDialog()
+  }
+
+  private moveIsoExtractionToBackground(): void {
+    this.closeIsoDialog(false)
+    ToastUtil.showToast('ISO 提取已转入后台')
   }
 
   private async extractSelectedIsoAudioFiles(): Promise<void> {
@@ -8200,47 +8220,38 @@ export struct LocalMusic {
       return
     }
     if (ArrayUtil.isEmpty(this.selectedIsoAudioPaths)) {
-      ToastUtil.showToast('请先选择要提取的音频文件')
+      ToastUtil.showToast('请先选择要提取的文件')
       return
     }
     const selectedPathSet: Set<string> = new Set<string>(this.selectedIsoAudioPaths)
     const selectedEntries = this.isoAudioEntries
       .filter((entry: IsoArchiveAudioEntry) => selectedPathSet.has(entry.path))
     if (ArrayUtil.isEmpty(selectedEntries)) {
-      ToastUtil.showToast('未找到要提取的音频文件')
+      ToastUtil.showToast('未找到要提取的文件')
       return
     }
     const hasSacdTrack: boolean = selectedEntries.some((entry: IsoArchiveAudioEntry) => entry.entryType === 'sacd_track')
+    const destinationDir: string = this.currentPath
 
     this.isIsoExtracting = true
     this.isoExtractCanceled = false
     this.isoExtractCancelMessage = ''
+    this.isoExtractDestinationDir = destinationDir
     Logger.info(TAG,
-      `heanup ISO extract start file=${this.isoSourceFilePath}, selected=${selectedEntries.length}, hasSacd=${hasSacdTrack}`)
+      `heanup ISO extract start file=${this.isoSourceFilePath}, selected=${selectedEntries.length}, ` +
+        `hasSacd=${hasSacdTrack}, destinationDir=${destinationDir}`)
     Logger.info(TAG,
       `heanup ISO selected entries: ${selectedEntries.map((entry: IsoArchiveAudioEntry) => entry.path).join(', ')}`)
-    this.loadingProgressDialogId = DialogHelper.showLoadingProgress({
-      progress: 0,
-      backCancel: true,
-      autoCancel: false,
-      loadColor: this.themeColor,
-      fontColor: this.themeColor,
-      onWillDismiss: (dismissDialogAction: DismissDialogAction) => {
-        if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
-          this.cancelIsoExtraction('已取消 ISO 提取', false)
-          dismissDialogAction.dismiss()
-        }
-      }
-    })
-    this.isoExtractDisplayedProgress = 0
-    this.updateIsoExtractProgressDialog(1, '准备提取 1%', true)
-    this.startIsoExtractProgressWatcher(selectedEntries)
+    this.resetIsoExtractProgressState()
+    this.updateIsoExtractProgressState(1, '准备提取 1%', '', true)
+    this.startIsoExtractProgressWatcher(selectedEntries, destinationDir)
 
     try {
-      const outputPaths: string[] = this.buildIsoExtractExpectedOutputPaths(selectedEntries)
+      const extractStartedAt = Date.now()
+      const outputPaths: string[] = this.buildIsoExtractExpectedOutputPaths(selectedEntries, destinationDir)
       const payloadJson = JSON.stringify({
         isoPath: this.isoSourceFilePath,
-        destinationDir: this.currentPath,
+        destinationDir,
         entries: selectedEntries,
         outputPaths
       })
@@ -8261,17 +8272,17 @@ export struct LocalMusic {
         }
         const stageText = progressData.stage === 'prepare' ? '准备提取' :
           (progressData.stage === 'done' ? '提取完成' : (progressData.stage === 'error' ? '提取异常' : '正在提取'))
-        const nameText = StrUtil.isNotEmpty(progressData.name) ? ` ${progressData.name}` : ''
         if (progress > this.isoExtractDisplayedProgress || progressData.stage !== 'extract') {
           Logger.info(TAG,
             `heanup ISO 提取任务进度: stage=${progressData.stage || ''}, progress=${progress}, ` +
               `name=${progressData.name || ''}`)
         }
-        this.updateIsoExtractProgressDialog(progress, `${stageText} ${progress}%${nameText}`)
+        this.updateIsoExtractProgressState(progress, `${stageText} ${progress}%`, progressData.name || '')
       })
       const result = await taskpool.execute(task, taskpool.Priority.HIGH) as IsoExtractTaskResult
+      const extractElapsedMs = Date.now() - extractStartedAt
       this.isoExtractTask = undefined
-      Logger.info(TAG, `heanup ISO extract task completed canceled=${this.isoExtractCanceled}`)
+      Logger.info(TAG, `heanup ISO extract task completed canceled=${this.isoExtractCanceled}, elapsedMs=${extractElapsedMs}`)
       if (this.isoExtractCanceled) {
         return
       }
@@ -8280,15 +8291,41 @@ export struct LocalMusic {
       const failureDetails = result?.failureDetails || []
       Logger.info(TAG,
         `heanup ISO extract result extracted=${extractedPaths.length}, failed=${failedEntries.length}, details=${failureDetails.length}`)
+
+      let importErrorMessage: string = ''
+      let importedCount: number = 0
       if (ArrayUtil.isNotEmpty(extractedPaths)) {
-        await this.importExtractedFilesToCurrentPath(extractedPaths)
+        const importStartedAt = Date.now()
+        try {
+          await this.postProcessExtractedIsoFiles(extractedPaths, selectedEntries, outputPaths)
+          importedCount = await this.importExtractedFilesToDirectory(extractedPaths, destinationDir)
+          Logger.info(TAG,
+            `heanup ISO import extracted files success elapsedMs=${Date.now() - importStartedAt}, ` +
+              `importedCount=${importedCount}`)
+        } catch (importError) {
+          importErrorMessage = (importError as Error).message || '导入媒体库失败'
+          Logger.error(TAG,
+            `heanup ISO import extracted files failed elapsedMs=${Date.now() - importStartedAt}, ` +
+              `message=${importErrorMessage}`)
+        }
       }
-      this.closeIsoDialog()
 
       if (extractedPaths.length > 0 && failedEntries.length > 0) {
-        ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个`)
+        if (StrUtil.isNotEmpty(importErrorMessage)) {
+          ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个,导入失败`)
+        } else if (importedCount > 0) {
+          ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个,已入库 ${importedCount} 个音频文件`)
+        } else {
+          ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个`)
+        }
       } else if (extractedPaths.length > 0) {
-        ToastUtil.showToast(`提取完成,已导入 ${extractedPaths.length} 个音频文件`)
+        if (StrUtil.isNotEmpty(importErrorMessage)) {
+          ToastUtil.showToast(`提取完成,但导入媒体库失败: ${importErrorMessage}`)
+        } else if (importedCount > 0) {
+          ToastUtil.showToast(`提取完成,已提取 ${extractedPaths.length} 个文件,入库 ${importedCount} 个音频文件`)
+        } else {
+          ToastUtil.showToast(`提取完成,已提取 ${extractedPaths.length} 个文件`)
+        }
       } else if (ArrayUtil.isNotEmpty(failureDetails)) {
         const firstFailure: IsoExtractFailureDetail = failureDetails[0]
         const reasonText = firstFailure.reason && firstFailure.reason.length > 0 ? firstFailure.reason : '提取失败'
@@ -8303,9 +8340,10 @@ export struct LocalMusic {
       if (this.isoExtractCanceled) {
         return
       }
-      Logger.error(TAG, `heanup 提取 ISO 音频失败: ${(error as Error).message}`)
-      ToastUtil.showToast('提取 ISO 音频失败')
+      Logger.error(TAG, `heanup 提取 ISO 文件失败: ${(error as Error).message}`)
+      ToastUtil.showToast('提取 ISO 文件失败')
     } finally {
+      const shouldResetDialogState: boolean = this.isoComponentId <= 0
       this.stopIsoExtractProgressWatcher()
       this.isoExtractTask = undefined
       this.isIsoExtracting = false
@@ -8316,18 +8354,145 @@ export struct LocalMusic {
       }
       this.isoExtractCanceled = false
       this.isoExtractCancelMessage = ''
-      this.closeIsoExtractLoadingDialog()
+      if (shouldResetDialogState) {
+        this.resetIsoDialogState()
+      } else {
+        this.selectedIsoAudioPaths = []
+        this.resetIsoExtractProgressState()
+      }
     }
   }
 
-  private async importExtractedFilesToCurrentPath(paths: Array<string>): Promise<void> {
-    if (ArrayUtil.isEmpty(paths)) {
+  private async postProcessExtractedIsoFiles(extractedPaths: Array<string>, selectedEntries: Array<IsoArchiveAudioEntry>,
+    outputPaths: Array<string>): Promise<void> {
+    if (ArrayUtil.isEmpty(extractedPaths) || ArrayUtil.isEmpty(selectedEntries) || ArrayUtil.isEmpty(outputPaths)) {
+      return
+    }
+
+    const entryByOutputPath: Map<string, IsoArchiveAudioEntry> = new Map<string, IsoArchiveAudioEntry>()
+    for (let i = 0; i < outputPaths.length && i < selectedEntries.length; i++) {
+      entryByOutputPath.set(outputPaths[i], selectedEntries[i])
+    }
+
+    for (let i = 0; i < extractedPaths.length; i++) {
+      const extractedPath = extractedPaths[i]
+      const entry = entryByOutputPath.get(extractedPath)
+      if (!entry) {
+        continue
+      }
+      await this.postProcessExtractedIsoEntry(extractedPath, entry)
+    }
+  }
+
+  private async postProcessExtractedIsoEntry(filePath: string, entry: IsoArchiveAudioEntry): Promise<void> {
+    if (!Utility.isMusicByExtension(filePath)) {
+      return
+    }
+
+    const hasEmbeddedMetadata: boolean = StrUtil.isNotEmpty(entry.title) || StrUtil.isNotEmpty(entry.album) ||
+      StrUtil.isNotEmpty(entry.artist) || StrUtil.isNotEmpty(entry.albumArtist) || StrUtil.isNotEmpty(entry.comment) ||
+      !!(entry.trackNumber && entry.trackNumber > 0)
+    if (entry.entryType !== 'sacd_track' && !hasEmbeddedMetadata) {
+      return
+    }
+
+    const tags: FFMpegTags = this.buildIsoExtractedMetadata(entry)
+    if (Object.keys(tags).length > 0) {
+      try {
+        const metadataResult = await repairAudioMetadata(filePath, '', tags, true, '')
+        Logger.info(TAG,
+          `heanup ISO metadata post-process path=${filePath}, success=${metadataResult}, type=${entry.entryType || ''}`)
+      } catch (error) {
+        Logger.error(TAG, `heanup ISO metadata post-process failed path=${filePath}, message=${(error as Error).message}`)
+      }
+    }
+
+    if (!filePath.toLowerCase().endsWith('.dsf')) {
       return
     }
 
+    const coverUrl = await this.resolveIsoExtractCoverUrl(entry)
+    if (StrUtil.isEmpty(coverUrl)) {
+      return
+    }
+
+    try {
+      const coverResult = await changeMusicCover(this.context, filePath, coverUrl, true, '')
+      Logger.info(TAG, `heanup ISO cover post-process path=${filePath}, success=${coverResult}`)
+    } catch (error) {
+      Logger.error(TAG, `heanup ISO cover post-process failed path=${filePath}, message=${(error as Error).message}`)
+    }
+  }
+
+  private buildIsoExtractedMetadata(entry: IsoArchiveAudioEntry): FFMpegTags {
+    const metadata: FFMpegTags = {}
+    const title = entry.title || this.getIsoEntryBaseName(entry.name || entry.path)
+    if (StrUtil.isNotEmpty(title)) {
+      metadata.title = title
+      metadata.TITLE = title
+    }
+    if (StrUtil.isNotEmpty(entry.artist)) {
+      metadata.artist = entry.artist
+      metadata.ARTIST = entry.artist
+    }
+    if (StrUtil.isNotEmpty(entry.album)) {
+      metadata.album = entry.album
+      metadata.ALBUM = entry.album
+    }
+    if (entry.trackNumber && entry.trackNumber > 0) {
+      metadata.track = `${entry.trackNumber}`
+      metadata.TRACK = `${entry.trackNumber}`
+    }
+    if (StrUtil.isNotEmpty(entry.albumArtist)) {
+      metadata.ALBUMARTIST = entry.albumArtist
+      metadata.album_artist = entry.albumArtist
+      metadata.TPE2 = entry.albumArtist
+    }
+    if (StrUtil.isNotEmpty(entry.comment)) {
+      metadata.comment = entry.comment
+      metadata.COMMENT = entry.comment
+      metadata.COMM = entry.comment
+    }
+    return metadata
+  }
+
+  private getIsoEntryBaseName(fileName: string): string {
+    const lastSegment = this.getIsoLastPathSegment(fileName)
+    const lastDot = lastSegment.lastIndexOf('.')
+    if (lastDot <= 0) {
+      return lastSegment
+    }
+    return lastSegment.substring(0, lastDot)
+  }
+
+  private async resolveIsoExtractCoverUrl(entry: IsoArchiveAudioEntry): Promise<string> {
+    const title = entry.title || this.getIsoEntryBaseName(entry.name || entry.path)
+    const artist = entry.artist || entry.albumArtist || ''
+    if (StrUtil.isEmpty(title) || PreferencesUtil.getStringSync('COVER_API', '') === '') {
+      return ''
+    }
+
+    try {
+      const coverUrl = await NetAxiosUtil.getLyricCover(title, artist, PreferencesUtil.getStringSync('COVER_API', ''))
+      if (StrUtil.isNotEmpty(coverUrl) && coverUrl !== 'unknown') {
+        return coverUrl
+      }
+    } catch (error) {
+      Logger.error(TAG,
+        `heanup ISO resolve cover failed title=${title}, artist=${artist}, message=${(error as Error).message}`)
+    }
+    return ''
+  }
+
+  private async importExtractedFilesToDirectory(paths: Array<string>, destinationDir: string): Promise<number> {
+    if (ArrayUtil.isEmpty(paths)) {
+      return 0
+    }
+
+    let importedCount: number = 0
     for (let i = 0; i < paths.length; i++) {
       const filePath = paths[i]
-      if (this.currentPath === this.lockPath || !Utility.isMeidaByExtension(filePath) ||
+      if (destinationDir === this.lockPath || !Utility.isMeidaByExtension(filePath) ||
         filePath.endsWith('.lrc') || filePath.endsWith('.srt') || Utility.isIsoByExtension(filePath)) {
         continue
       }
@@ -8337,17 +8502,33 @@ export struct LocalMusic {
       await new Promise<void>((resolve) => {
         this.table.insert(mediaItem, (_id: number) => resolve())
       })
+      importedCount++
     }
 
-    this.cache.delete(this.currentPath)
-    this.pageCache.clear()
-    this.pageHasMoreCache.clear()
-    this.pageTotalCountCache.clear()
-    if (this.currentPath === this.lockPath) {
-      this.getSortedFiles(this.currentPath, true)
-    } else {
-      await this.resetAndLoadFirstPage()
+    this.cache.delete(destinationDir)
+    if (this.currentPath === destinationDir) {
+      this.pageCache.clear()
+      this.pageHasMoreCache.clear()
+      this.pageTotalCountCache.clear()
+      if (destinationDir === this.lockPath) {
+        this.getSortedFiles(destinationDir, true)
+      } else {
+        await this.resetAndLoadFirstPage()
+      }
     }
+    return importedCount
+  }
+
+  private resetIsoDialogState(): void {
+    this.isoAudioEntries = []
+    this.selectedIsoAudioPaths = []
+    this.isoSourceFileName = ''
+    this.isoSourceFilePath = ''
+    this.isoExtractDestinationDir = ''
+    this.isIsoExtracting = false
+    this.stopIsoExtractProgressWatcher()
+    this.isoExtractTask = undefined
+    this.resetIsoExtractProgressState()
   }
 
   private closeIsoDialog(reset: boolean = true): void {
@@ -8361,13 +8542,7 @@ export struct LocalMusic {
     if (!reset) {
       return
     }
-    this.isoAudioEntries = []
-    this.selectedIsoAudioPaths = []
-    this.isoSourceFileName = ''
-    this.isoSourceFilePath = ''
-    this.isIsoExtracting = false
-    this.stopIsoExtractProgressWatcher()
-    this.isoExtractTask = undefined
+    this.resetIsoDialogState()
   }
 
   @Builder
@@ -8378,6 +8553,9 @@ export struct LocalMusic {
       selectedPaths: this.selectedIsoAudioPaths,
       isLongNameRoLL: this.isLongNameRoLL,
       isExtracting: this.isIsoExtracting,
+      extractProgress: this.isoExtractProgress,
+      extractProgressText: this.isoExtractProgressText,
+      extractCurrentName: this.isoExtractCurrentEntryName,
       onToggleSelection: (path: string, checked: boolean) => {
         this.toggleIsoEntrySelection(path, checked)
       },
@@ -8387,9 +8565,15 @@ export struct LocalMusic {
       onExtract: async () => {
         await this.extractSelectedIsoAudioFiles()
       },
+      onRunInBackground: () => {
+        this.moveIsoExtractionToBackground()
+      },
+      onStopExtract: () => {
+        this.cancelIsoExtraction('已取消 ISO 提取')
+      },
       onCancel: () => {
         if (this.isIsoExtracting) {
-          this.cancelIsoExtraction('已取消 ISO 提取')
+          this.moveIsoExtractionToBackground()
           return
         }
         this.closeIsoDialog()
@@ -8526,8 +8710,9 @@ export struct LocalMusic {
       curve: Curve.Friction
     }, () => {
       this.isShowPlay = true;
+      this.showCoverScaleGuideIfNeeded()
     });
-    this.showCoverScaleGuideIfNeeded()
+
   }
 
   setShowPlayFalse(){
@@ -8541,6 +8726,7 @@ export struct LocalMusic {
   }
   //打开播放页
   showPlayerView() {
+    this.showCoverScaleGuideIfNeeded()
     console.info('onecold  showPlayerView  this.songList'+ JSON.stringify(this.songList))
     if (ArrayUtil.isNotEmpty(this.songList)) {
       // this.setShowPlayTrue()
@@ -11641,16 +11827,10 @@ export struct LocalMusic {
         .width('65%')
 
         Column() {
-          ImageAnimator()
-            .images(this.imagesF)// 动画数组
-            .duration(1000)// 持续
-            .state(this.animationState)// 动画状态
-            .fillMode(FillMode.Forwards)
-            .width(18)
-            .visibility(this.videoUrl === item.filePath ? this.isMultiSelect ? Visibility.None : Visibility.Visible :
-              Visibility.None)
-            .height(18)
-            .iterations(-1) // 播放次数
+          PlayingIndicator({
+            isActive: this.videoUrl === item.filePath && !this.isMultiSelect,
+            indicatorSize: 18
+          })
         }
         .width('20%')
 
@@ -12043,16 +12223,6 @@ export struct LocalMusic {
                     }),
 
 
-                  //双击屏幕暂停或者播放
-                  TapGesture({ count: 2 })
-                    .onAction((event: GestureEvent) => {
-                      if (this.CONTROL_PlayStatus === PlayStatus.PLAY) {
-                        ToastUtil.showToast('已暂停')
-                      }
-                      console.info('onecold playOrPause 6693')
-                      this.playOrPause()
-
-                    })
 
                 )
 
@@ -13690,9 +13860,9 @@ export struct LocalMusic {
         .justifyContent(FlexAlign.Start)
         .zIndex(1)
         .visibility(this.isCoverOpacity() ? Visibility.Visible : Visibility.None)
-        if(this.isShowCoverScaleGuide){
-          this.CoverScaleGuideOverlay()
-        }
+
+        this.CoverScaleGuideOverlay()
+
 
 
       }
@@ -15447,10 +15617,10 @@ export struct LocalMusic {
                       this.isCoverRectangle = checked;
                       PreferencesUtil.put(SettingPage.IS_COVER_RECTANGLE, this.isCoverRectangle)
                       if (this.isCoverRectangle) {
-                        ToastUtil.showToast('封面方形已开启')
+                        ToastUtil.showToast('封面方形已开启,双指缩放封面可以调整封面大小')
                         this.animationRoFun()
                       } else {
-                        ToastUtil.showToast('封面圆形已开启')
+                        ToastUtil.showToast('封面圆形已开启,双指缩放封面可以调整封面大小')
                         this.animationRoFun()
                       }
                     }
@@ -15479,6 +15649,7 @@ export struct LocalMusic {
                 Select([
                   { value: '柱状' },
                   { value: '波形' },
+                  { value: '水波纹' },
                   { value: '圆形' },
                   { value: '粒子' },
                   { value: '脉冲' },
@@ -15486,7 +15657,8 @@ export struct LocalMusic {
                   { value: '影子柱' },
                   { value: '轨道线' },
                   { value: '点阵' },
-                  { value: '星爆' }
+                  { value: '星爆' },
+                  { value: '心电图' }
                 ])
                   .font({ size: 14, weight: FontWeight.Medium })
                   .fontColor(Color.White)
@@ -15494,13 +15666,13 @@ export struct LocalMusic {
                   .margin({top:more.id >= 22?3:0})
                   .visibility(more.id === 26 ? Visibility.Visible : Visibility.None)
                   .selected(this.spectrumModeIndex)
-                  .value(['柱状', '波形', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆'][this.spectrumModeIndex])
+                  .value(['柱状', '波形', '水波纹', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆', '心电图'][this.spectrumModeIndex])
                   .onSelect((index: number, _text?: string) => {
                     this.spectrumModeIndex = index;
                     // 使用同步方法保存,确保立即生效
                     PreferencesUtil.putSync('spectrumModeIndex', this.spectrumModeIndex);
                     AppStorage.setOrCreate<number>('spectrumMode', this.spectrumModeIndex);
-                    const modeNames = ['柱状', '波形', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆'];
+                    const modeNames = ['柱状', '波形', '水波纹', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆', '心电图'];
                     Logger.info('heanup-LocalMusic', '频谱特效已切换为: ' + modeNames[index]);
                   });
 
@@ -18290,7 +18462,7 @@ export struct LocalMusic {
   }
 
   private cycleSpectrumMode(): void {
-    const modeNames: string[] = ['柱状', '波形', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆'];
+    const modeNames: string[] = ['柱状', '波形', '水波纹', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆', '心电图'];
     if (modeNames.length <= 0) {
       return;
     }

+ 153 - 0
entry/src/main/ets/view/PlayingIndicator.ets

@@ -0,0 +1,153 @@
+import { CommonConstants } from '../common/constants/CommonConstants'
+import { normalizeHexColor, rgbaFromHex } from './spectrum/SpectrumRenderer'
+//正在播放的动画指示器
+@Component
+export struct PlayingIndicator {
+  @Prop @Watch('onActiveChange') isActive: boolean = false
+  @Prop indicatorSize: number = 18
+  @Prop marginRight: number = 0
+  @Prop marginTop: number = 0
+  @Prop marginBottom: number = 0
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
+
+  private readonly renderSettings: RenderingContextSettings = new RenderingContextSettings(true)
+  private readonly canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderSettings)
+
+  private timerId: number = -1
+  private phase: number = 0
+  private canvasWidth: number = 0
+  private canvasHeight: number = 0
+
+  aboutToAppear(): void {
+    this.updateAnimationState()
+    this.drawFrame()
+  }
+
+  aboutToDisappear(): void {
+    this.stopTimer()
+  }
+
+  onActiveChange(): void {
+    this.updateAnimationState()
+    this.drawFrame()
+  }
+
+  private updateAnimationState(): void {
+    if (this.isActive) {
+      this.startTimer()
+      return
+    }
+    this.stopTimer()
+    this.phase = 0
+  }
+
+  private startTimer(): void {
+    if (this.timerId !== -1) {
+      return
+    }
+    this.timerId = setInterval(() => {
+      this.phase += 0.58
+      if (this.phase > Math.PI * 2) {
+        this.phase -= Math.PI * 2
+      }
+      this.drawFrame()
+    }, 88)
+  }
+
+  private stopTimer(): void {
+    if (this.timerId === -1) {
+      return
+    }
+    clearInterval(this.timerId)
+    this.timerId = -1
+  }
+
+  private drawFrame(): void {
+    if (this.canvasWidth <= 0 || this.canvasHeight <= 0) {
+      return
+    }
+
+    const ctx: CanvasRenderingContext2D = this.canvasContext
+    ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+    if (!this.isActive) {
+      return
+    }
+
+    const brandColor: string = normalizeHexColor(this.themeColor, CommonConstants.DEFAULT_THEME_COLOR)
+    const barCount: number = 5
+    const barWidth: number = Math.max(1.0, Math.min(1.8, this.canvasWidth * 0.075))
+    const barGap: number = Math.max(0.9, Math.min(1.7, this.canvasWidth * 0.082))
+    const totalWidth: number = barWidth * barCount + barGap * (barCount - 1)
+    const startX: number = (this.canvasWidth - totalWidth) / 2
+    const baseY: number = this.canvasHeight
+    const minBarHeight: number = this.canvasHeight * 0.28
+    const maxBarHeight: number = this.canvasHeight * 0.84
+
+    for (let i = 0; i < barCount; i++) {
+      const primaryWave: number = 0.5 + 0.5 * Math.sin(this.phase + i * 0.82)
+      const secondaryWave: number = 0.5 + 0.5 * Math.sin(this.phase * 0.65 + i * 1.24 + 1.1)
+      const level: number = 0.18 + 0.82 * (primaryWave * 0.72 + secondaryWave * 0.28)
+      const barHeight: number = minBarHeight + (maxBarHeight - minBarHeight) * level
+      const x: number = startX + i * (barWidth + barGap)
+      const y: number = baseY - barHeight
+
+      const topColor: string = rgbaFromHex(brandColor, 0.96)
+      const bottomColor: string = rgbaFromHex(brandColor, 0.62)
+      const glowColor: string = rgbaFromHex(brandColor, 0.12)
+      const barGradient: CanvasGradient = ctx.createLinearGradient(x, y, x, baseY)
+      barGradient.addColorStop(0, topColor)
+      barGradient.addColorStop(1, bottomColor)
+
+      ctx.fillStyle = glowColor
+      this.fillRoundedRect(ctx, x - 0.35, y - 0.35, barWidth + 0.7, barHeight + 0.7, barWidth * 0.50)
+      ctx.fillStyle = barGradient
+      this.fillRoundedRect(ctx, x, y, barWidth, barHeight, barWidth * 0.50)
+    }
+  }
+
+  private fillRoundedRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number,
+    radius: number): void {
+    const safeRadius: number = Math.min(radius, width * 0.5, height * 0.5)
+    ctx.beginPath()
+    ctx.moveTo(x + safeRadius, y)
+    ctx.lineTo(x + width - safeRadius, y)
+    ctx.quadraticCurveTo(x + width, y, x + width, y + safeRadius)
+    ctx.lineTo(x + width, y + height - safeRadius)
+    ctx.quadraticCurveTo(x + width, y + height, x + width - safeRadius, y + height)
+    ctx.lineTo(x + safeRadius, y + height)
+    ctx.quadraticCurveTo(x, y + height, x, y + height - safeRadius)
+    ctx.lineTo(x, y + safeRadius)
+    ctx.quadraticCurveTo(x, y, x + safeRadius, y)
+    ctx.closePath()
+    ctx.fill()
+  }
+
+  build() {
+    Row() {
+      Canvas(this.canvasContext)
+        .width(this.indicatorSize)
+        .height(this.indicatorSize)
+        .onReady(() => {
+          this.canvasContext.imageSmoothingEnabled = true
+          this.canvasContext.imageSmoothingQuality = 'medium'
+          this.drawFrame()
+        })
+        .onAreaChange((_oldVal: Area, newVal: Area) => {
+          this.canvasWidth = newVal.width as number
+          this.canvasHeight = newVal.height as number
+          this.drawFrame()
+        })
+    }
+    .width(this.indicatorSize)
+    .height(this.indicatorSize)
+    .margin({ right: this.marginRight, top: this.marginTop, bottom: this.marginBottom })
+    .visibility(this.isActive ? Visibility.Visible : Visibility.None)
+    .onAppear(() => {
+      this.updateAnimationState()
+      this.drawFrame()
+    })
+    .onDisAppear(() => {
+      this.stopTimer()
+    })
+  }
+}

+ 38 - 26
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -6,6 +6,7 @@ import {
 } from '@pura/harmony-utils';
 import {
   ButtonFancyModifier,
+  MenuModifier,
   SymbolGlyphFancyModifier,
   ShadowModifier
 } from '../common/util/AttributeModifierUtil';
@@ -34,6 +35,7 @@ import NavidromeListCache, { AllCacheData } from '../common/util/NavidromeListCa
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { taskpool } from '@kit.ArkTS';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
+import { PlayingIndicator } from './PlayingIndicator';
 
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 const NAVIDROME_SEARCH_LIMIT = 500;
@@ -219,7 +221,6 @@ export struct RemoteMusicPage {
   @Link mType: number;
   @Link offsetX: number;
   @Link isShowDrawer: boolean;
-  @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
   @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
   @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
   @StorageProp('currentTheme') currentTheme: number = 0;
@@ -383,7 +384,6 @@ export struct RemoteMusicPage {
     this.customizeBgPath  = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '')
     this.blurValue = PreferencesUtil.getNumberSync(SettingPage.CUSTOMIZE_BG_BLUR, 0)
     this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0)
-    this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true)
     this.sortType = PreferencesUtil.getNumberSync('navidromeSortType', 0);
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
     this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true)
@@ -2770,24 +2770,30 @@ export struct RemoteMusicPage {
     Menu() {
       MenuItem({
         symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')),
-        content: $r('app.string.sort_by_name')
+        content: $r('app.string.sort_by_name'),
+        symbolEndIcon: this.sortType === 0 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
       })
+        .backgroundColor(this.getSortItemBackground(0))
         .onClick(async () => {
           this.doSortType(0);
           PreferencesUtil.put("navidromeSortType", 0);
         })
       MenuItem({
         symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')),
-        content: '按名称降序'
+        content: '按名称降序',
+        symbolEndIcon: this.sortType === 1 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
       })
+        .backgroundColor(this.getSortItemBackground(1))
         .onClick(async () => {
           this.doSortType(1);
           PreferencesUtil.put("navidromeSortType", 1);
         })
       MenuItem({
         symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')),
-        content: '按艺术家升序'
+        content: '按艺术家升序',
+        symbolEndIcon: this.sortType === 2 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
       })
+        .backgroundColor(this.getSortItemBackground(2))
         .onClick(async () => {
           this.doSortType(2);
           PreferencesUtil.put("navidromeSortType", 2);
@@ -2795,16 +2801,20 @@ export struct RemoteMusicPage {
 
       MenuItem({
         symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')),
-        content: '按艺术家降序'
+        content: '按艺术家降序',
+        symbolEndIcon: this.sortType === 3 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
       })
+        .backgroundColor(this.getSortItemBackground(3))
         .onClick(async () => {
           this.doSortType(3);
           PreferencesUtil.put("navidromeSortType", 3);
         })
       MenuItem({
         symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')),
-        content: '按专辑升序'
+        content: '按专辑升序',
+        symbolEndIcon: this.sortType === 4 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
       })
+        .backgroundColor(this.getSortItemBackground(4))
         .onClick(async () => {
           this.doSortType(4);
           PreferencesUtil.put("navidromeSortType", 4);
@@ -2812,13 +2822,26 @@ export struct RemoteMusicPage {
 
       MenuItem({
         symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')),
-        content: '按专辑降序'
+        content: '按专辑降序',
+        symbolEndIcon: this.sortType === 5 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
       })
+        .backgroundColor(this.getSortItemBackground(5))
         .onClick(async () => {
           this.doSortType(5);
           PreferencesUtil.put("navidromeSortType", 5);
         })
+    }.attributeModifier(new MenuModifier())
+  }
+
+  private getSortItemBackground(sortType: number): ResourceColor {
+    if (this.sortType !== sortType) {
+      return Color.Transparent;
+    }
+    const isAsc: boolean = sortType % 2 === 0;
+    if (isAsc) {
+      return this.isDarkMode ? '#295B8A' : '#DCEEFF';
     }
+    return this.isDarkMode ? '#7A4D22' : '#FFE9D5';
   }
 
   doSortType(index: number) {
@@ -3103,17 +3126,13 @@ export struct RemoteMusicPage {
         .padding({ right: 20 })
 
         Column() {
-          ImageAnimator()
-            .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组
-            .duration(1000)// 持续
-            .state(AnimationStatus.Running)// 动画状态
-            .fillMode(FillMode.Forwards)
-            .visibility(this.currentSong?.filePath == song.filePath ? Visibility.Visible :
-              Visibility.None)
-            .width(18)
-            .margin({ right: 12, top: 8, bottom: 8 })
-            .height(18)
-            .iterations(-1) // 播放次数
+          PlayingIndicator({
+            isActive: this.currentSong?.filePath == song.filePath,
+            indicatorSize: 18,
+            marginRight: 12,
+            marginTop: 8,
+            marginBottom: 8
+          })
         }
       }
     }
@@ -3636,13 +3655,6 @@ export struct RemoteMusicPage {
       void ServerLogUtil.info('StreamingPlay', `播放事件已发送`);
       void ServerLogUtil.debug('StreamingPlay', `播放事件详情: 列表ID=${playlistData.playlistId}, 列表名称=${playlistData.playlistName}, 歌曲数=${playlistData.songCount}, 起始索引=${playlistData.startIndex}, 是否跳转=${playlistData.isJump}`);
 
-      if(!this.isNoJumpToHome){
-        void ServerLogUtil.debug('StreamingPlay', '跳转到首页播放器');
-        // 跳转到首页播放器
-        this.getUIContext()?.animateTo({ duration: 555 }, () => {
-          this.mType = 0
-        })
-      }
 
     } catch (error) {
       const err = error as Error;

+ 141 - 0
entry/src/main/ets/view/spectrum/EcgSpectrumRenderer.ets

@@ -0,0 +1,141 @@
+import { SpectrumRenderer, SpectrumRenderOptions, buildSpectrumPalette, hslToHex, rgbaFromHex } from './SpectrumRenderer'
+
+/**
+ * 心电图频谱渲染器
+ * ECG 风格单线波形,带渐变描边和轻微辉光
+ */
+export class EcgSpectrumRenderer implements SpectrumRenderer {
+  private phase: number = 0
+
+  render(ctx: CanvasRenderingContext2D, bands: number[], energy: number,
+    width: number, height: number, brandColor: string, options?: SpectrumRenderOptions): void {
+    const bandCount: number = bands.length
+    if (bandCount === 0) {
+      return
+    }
+
+    ctx.clearRect(0, 0, width, height)
+
+    const palette = buildSpectrumPalette(brandColor)
+    const complexityScale: number = Math.max(0.45, Math.min(1, options?.complexityScale ?? 1))
+    const beatCount: number = Math.max(2, Math.min(4, Math.floor(2 + complexityScale * 2)))
+    const centerY: number = height * 0.56
+    const maxAmp: number = height * (0.16 + energy * 0.16)
+    const microAmp: number = height * (0.010 + energy * 0.018)
+    const lineColor: string = hslToHex(palette.coolHue, 0.92, 0.62)
+    const accentColor: string = hslToHex(palette.accentHue, 0.90, 0.58)
+    const warmColor: string = hslToHex(palette.warmHue, 0.82, 0.54)
+
+    this.phase += 0.010 + energy * 0.028
+    if (this.phase >= 1) {
+      this.phase -= 1
+    }
+
+    const baselineGradient: CanvasGradient = ctx.createLinearGradient(0, centerY - height * 0.14, 0, centerY + height * 0.12)
+    baselineGradient.addColorStop(0, rgbaFromHex(lineColor, 0))
+    baselineGradient.addColorStop(0.48, rgbaFromHex(lineColor, 0.08 + energy * 0.05))
+    baselineGradient.addColorStop(0.52, rgbaFromHex(accentColor, 0.18 + energy * 0.08))
+    baselineGradient.addColorStop(1, rgbaFromHex(lineColor, 0))
+    ctx.fillStyle = baselineGradient
+    ctx.fillRect(0, centerY - height * 0.16, width, height * 0.30)
+
+    const lineGradient: CanvasGradient = ctx.createLinearGradient(0, 0, width, 0)
+    lineGradient.addColorStop(0, rgbaFromHex(warmColor, 0.84))
+    lineGradient.addColorStop(0.35, rgbaFromHex(accentColor, 0.96))
+    lineGradient.addColorStop(0.7, rgbaFromHex(lineColor, 0.96))
+    lineGradient.addColorStop(1, rgbaFromHex(warmColor, 0.78))
+
+    const glowGradient: CanvasGradient = ctx.createLinearGradient(0, 0, width, 0)
+    glowGradient.addColorStop(0, rgbaFromHex(warmColor, 0.16))
+    glowGradient.addColorStop(0.5, rgbaFromHex(accentColor, 0.26 + energy * 0.10))
+    glowGradient.addColorStop(1, rgbaFromHex(lineColor, 0.14))
+
+    const pointsX: number[] = []
+    const pointsY: number[] = []
+    const stepX: number = bandCount > 1 ? width / (bandCount - 1) : width
+
+    for (let i = 0; i < bandCount; i++) {
+      const progress: number = bandCount > 1 ? i / (bandCount - 1) : 0
+      const x: number = i * stepX
+      const cycleProgress: number = (progress * beatCount + this.phase) % 1
+      const bandValue: number = Math.pow(Math.max(0, Math.min(1, bands[i])), 0.82)
+      const envelope: number = 0.35 + bandValue * 0.95 + energy * 0.20
+      const ecgShape: number = this.getEcgShape(cycleProgress)
+      const flutter: number = Math.sin(progress * Math.PI * (18 + beatCount * 2) + this.phase * Math.PI * 2) * microAmp * (0.3 + bandValue * 0.7)
+      const y: number = centerY - ecgShape * maxAmp * envelope + flutter
+      pointsX.push(x)
+      pointsY.push(y)
+    }
+
+    ctx.strokeStyle = rgbaFromHex(lineColor, 0.20)
+    ctx.lineWidth = 1
+    ctx.beginPath()
+    ctx.moveTo(0, centerY)
+    ctx.lineTo(width, centerY)
+    ctx.stroke()
+
+    this.strokePath(ctx, pointsX, pointsY, glowGradient, 10 + energy * 8)
+    this.strokePath(ctx, pointsX, pointsY, glowGradient, 6 + energy * 4)
+    this.strokePath(ctx, pointsX, pointsY, lineGradient, 2.2 + energy * 2.0)
+
+    const scanX: number = ((this.phase * 1.18) % 1) * width
+    const scanGradient: CanvasGradient = ctx.createLinearGradient(scanX - 18, 0, scanX + 18, 0)
+    scanGradient.addColorStop(0, 'rgba(255,255,255,0)')
+    scanGradient.addColorStop(0.5, rgbaFromHex(accentColor, 0.22 + energy * 0.10))
+    scanGradient.addColorStop(1, 'rgba(255,255,255,0)')
+    ctx.fillStyle = scanGradient
+    ctx.fillRect(Math.max(0, scanX - 18), 0, 36, height)
+  }
+
+  reset(): void {
+    this.phase = 0
+  }
+
+  private strokePath(ctx: CanvasRenderingContext2D, pointsX: number[], pointsY: number[],
+    strokeStyle: string | CanvasGradient, lineWidth: number): void {
+    if (pointsX.length === 0) {
+      return
+    }
+    ctx.beginPath()
+    ctx.moveTo(pointsX[0], pointsY[0])
+    for (let i = 1; i < pointsX.length; i++) {
+      const prevX: number = pointsX[i - 1]
+      const currX: number = pointsX[i]
+      const prevY: number = pointsY[i - 1]
+      const currY: number = pointsY[i]
+      const cpX: number = (prevX + currX) * 0.5
+      ctx.bezierCurveTo(cpX, prevY, cpX, currY, currX, currY)
+    }
+    ctx.strokeStyle = strokeStyle
+    ctx.lineWidth = lineWidth
+    ctx.stroke()
+  }
+
+  private getEcgShape(t: number): number {
+    if (t < 0.12) {
+      return 0
+    }
+    if (t < 0.20) {
+      return Math.sin((t - 0.12) / 0.08 * Math.PI) * 0.18
+    }
+    if (t < 0.28) {
+      return 0
+    }
+    if (t < 0.34) {
+      return -((t - 0.28) / 0.06) * 0.24
+    }
+    if (t < 0.39) {
+      return -0.24 + ((t - 0.34) / 0.05) * 1.50
+    }
+    if (t < 0.45) {
+      return 1.26 - ((t - 0.39) / 0.06) * 1.72
+    }
+    if (t < 0.58) {
+      return -0.46 + Math.sin((t - 0.45) / 0.13 * Math.PI) * 0.24
+    }
+    if (t < 0.74) {
+      return Math.sin((t - 0.58) / 0.16 * Math.PI) * 0.36
+    }
+    return 0
+  }
+}

+ 131 - 0
entry/src/main/ets/view/spectrum/RippleWaveSpectrumRenderer.ets

@@ -0,0 +1,131 @@
+import { SpectrumRenderer, SpectrumRenderOptions, buildSpectrumPalette, hslToHex, rgbaFromHex } from './SpectrumRenderer'
+
+/**
+ * 水波纹频谱渲染器
+ * 海浪式抛物线水波,中心浪峰更高,两侧自然回落
+ */
+export class RippleWaveSpectrumRenderer implements SpectrumRenderer {
+  private phase: number = 0
+
+  render(ctx: CanvasRenderingContext2D, bands: number[], energy: number,
+    width: number, height: number, brandColor: string, options?: SpectrumRenderOptions): void {
+    if (bands.length === 0) {
+      return
+    }
+
+    ctx.clearRect(0, 0, width, height)
+
+    const palette = buildSpectrumPalette(brandColor)
+    const complexityScale: number = Math.max(0.45, Math.min(1, options?.complexityScale ?? 1))
+    const layerCount: number = Math.max(4, Math.floor(7 * complexityScale))
+    const segmentCount: number = Math.max(44, Math.floor(92 * complexityScale))
+    const horizonY: number = height * 0.58
+    const waveHeight: number = height * (0.075 + energy * 0.13)
+    const layerGap: number = height * 0.05
+    const coolColor: string = hslToHex(palette.coolHue, 0.84, 0.66)
+    const primaryColor: string = hslToHex(palette.primaryHue, 0.78, 0.50)
+    const deepColor: string = hslToHex(palette.complementaryHue, 0.52, 0.26)
+
+    this.phase += 0.055 + energy * 0.05
+    if (this.phase > Math.PI * 2) {
+      this.phase -= Math.PI * 2
+    }
+
+    const skyGradient: CanvasGradient = ctx.createLinearGradient(0, 0, 0, height)
+    skyGradient.addColorStop(0, 'rgba(0, 0, 0, 0)')
+    skyGradient.addColorStop(0.62, rgbaFromHex(coolColor, 0.03 + energy * 0.02))
+    skyGradient.addColorStop(1, rgbaFromHex(primaryColor, 0.08 + energy * 0.05))
+    ctx.fillStyle = skyGradient
+    ctx.beginPath()
+    ctx.rect(0, 0, width, height)
+    ctx.fill()
+
+    for (let layer = 0; layer < layerCount; layer++) {
+      const layerProgress: number = layer / Math.max(1, layerCount - 1)
+      const baseY: number = horizonY + layer * layerGap
+      const localHeight: number = waveHeight * (1 - layerProgress * 0.5)
+      const strokeAlpha: number = 0.22 + (1 - layerProgress) * 0.24 + energy * 0.10
+      const fillAlpha: number = 0.05 + (1 - layerProgress) * 0.07 + energy * 0.04
+      const hue: number = (palette.coolHue + layer * 8) % 360
+      const lineWidth: number = 1.2 + (1 - layerProgress) * 1.6
+      const phaseShift: number = this.phase * (1.0 + layer * 0.08) + layer * 0.65
+
+      ctx.beginPath()
+      for (let i = 0; i <= segmentCount; i++) {
+        const progressX: number = i / segmentCount
+        const x: number = progressX * width
+        const normalizedX: number = progressX * 2 - 1
+        const parabolaEnvelope: number = Math.max(0, 1 - normalizedX * normalizedX)
+
+        const bandIndexFloat: number = progressX * (bands.length - 1)
+        const bandIndex: number = Math.min(bands.length - 1, Math.max(0, Math.floor(bandIndexFloat + layer)))
+        const bandValue: number = Math.pow(bands[bandIndex], 0.9)
+
+        const mainSwell: number = Math.sin(progressX * Math.PI * (2.0 + layer * 0.10) + phaseShift)
+        const detailSwell: number = Math.sin(progressX * Math.PI * (4.8 + layer * 0.22) - phaseShift * 0.82)
+        const crestShape: number = parabolaEnvelope * (0.52 + bandValue * 0.88)
+        const swellOffset: number = mainSwell * localHeight * 0.18 + detailSwell * localHeight * 0.06
+        const lift: number = crestShape * localHeight
+        const y: number = baseY - lift + swellOffset
+
+        if (i === 0) {
+          ctx.moveTo(x, y)
+        } else {
+          ctx.lineTo(x, y)
+        }
+      }
+
+      const fillGradient: CanvasGradient = ctx.createLinearGradient(0, baseY - localHeight * 1.1, 0, height)
+      fillGradient.addColorStop(0, rgbaFromHex(hslToHex(hue, 0.84, 0.64), fillAlpha))
+      fillGradient.addColorStop(0.55, rgbaFromHex(primaryColor, fillAlpha * 0.65))
+      fillGradient.addColorStop(1, rgbaFromHex(deepColor, 0.02))
+      ctx.lineTo(width, height)
+      ctx.lineTo(0, height)
+      ctx.closePath()
+      ctx.fillStyle = fillGradient
+      ctx.fill()
+
+      ctx.beginPath()
+      for (let i = 0; i <= segmentCount; i++) {
+        const progressX: number = i / segmentCount
+        const x: number = progressX * width
+        const normalizedX: number = progressX * 2 - 1
+        const parabolaEnvelope: number = Math.max(0, 1 - normalizedX * normalizedX)
+
+        const bandIndexFloat: number = progressX * (bands.length - 1)
+        const bandIndex: number = Math.min(bands.length - 1, Math.max(0, Math.floor(bandIndexFloat + layer)))
+        const bandValue: number = Math.pow(bands[bandIndex], 0.9)
+
+        const mainSwell: number = Math.sin(progressX * Math.PI * (2.0 + layer * 0.10) + phaseShift)
+        const detailSwell: number = Math.sin(progressX * Math.PI * (4.8 + layer * 0.22) - phaseShift * 0.82)
+        const crestShape: number = parabolaEnvelope * (0.52 + bandValue * 0.88)
+        const swellOffset: number = mainSwell * localHeight * 0.18 + detailSwell * localHeight * 0.06
+        const lift: number = crestShape * localHeight
+        const y: number = baseY - lift + swellOffset
+
+        if (i === 0) {
+          ctx.moveTo(x, y)
+        } else {
+          ctx.lineTo(x, y)
+        }
+      }
+      ctx.strokeStyle = rgbaFromHex(hslToHex(hue, 0.88, 0.70), strokeAlpha)
+      ctx.lineWidth = lineWidth
+      ctx.stroke()
+    }
+
+    const shimmerY: number = horizonY - waveHeight * (0.25 + energy * 0.15)
+    const shimmerGradient: CanvasGradient = ctx.createLinearGradient(0, shimmerY - 8, 0, shimmerY + 18)
+    shimmerGradient.addColorStop(0, 'rgba(255,255,255,0)')
+    shimmerGradient.addColorStop(0.5, rgbaFromHex(coolColor, 0.08 + energy * 0.05))
+    shimmerGradient.addColorStop(1, 'rgba(255,255,255,0)')
+    ctx.fillStyle = shimmerGradient
+    ctx.beginPath()
+    ctx.rect(0, shimmerY - 10, width, 28)
+    ctx.fill()
+  }
+
+  reset(): void {
+    this.phase = 0
+  }
+}

+ 16 - 0
entry/src/main/ets/view/spectrum/SpectrumEffectRegistry.ets

@@ -9,6 +9,8 @@ import { ShadowBarSpectrumRenderer } from './ShadowBarSpectrumRenderer'
 import { OrbitLineSpectrumRenderer } from './OrbitLineSpectrumRenderer'
 import { DotMatrixSpectrumRenderer } from './DotMatrixSpectrumRenderer'
 import { StarburstSpectrumRenderer } from './StarburstSpectrumRenderer'
+import { RippleWaveSpectrumRenderer } from './RippleWaveSpectrumRenderer'
+import { EcgSpectrumRenderer } from './EcgSpectrumRenderer'
 
 export type SpectrumPerformanceLevel = 'low' | 'medium' | 'high'
 
@@ -42,6 +44,13 @@ const EFFECTS: SpectrumEffectDefinition[] = [
     performance: { targetFps: 30, minFps: 18, maxBandCount: 32, complexityFloor: 0.62 },
     createRenderer: (): SpectrumRenderer => new WaveSpectrumRenderer()
   },
+  {
+    effectId: 'ripple-wave',
+    displayName: '水波纹',
+    performanceLevel: 'medium',
+    performance: { targetFps: 30, minFps: 18, maxBandCount: 28, complexityFloor: 0.58 },
+    createRenderer: (): SpectrumRenderer => new RippleWaveSpectrumRenderer()
+  },
   {
     effectId: 'circle',
     displayName: '圆形',
@@ -97,6 +106,13 @@ const EFFECTS: SpectrumEffectDefinition[] = [
     performanceLevel: 'medium',
     performance: { targetFps: 30, minFps: 18, maxBandCount: 30, complexityFloor: 0.58 },
     createRenderer: (): SpectrumRenderer => new StarburstSpectrumRenderer()
+  },
+  {
+    effectId: 'ecg-wave',
+    displayName: '心电图',
+    performanceLevel: 'medium',
+    performance: { targetFps: 30, minFps: 18, maxBandCount: 32, complexityFloor: 0.60 },
+    createRenderer: (): SpectrumRenderer => new EcgSpectrumRenderer()
   }
 ]
 

+ 2 - 2
entry/src/main/ets/viewmodel/MainViewModel.ets

@@ -52,12 +52,12 @@ export  class  MainViewModel{
       new ItemData($r('app.string.media_ku'), $r('app.media.hm_playlist'),MainViewModel.MENU_MIEDIA_KU,false),
       new ItemData($r('app.string.artist'), $r('app.media.kp_music'),MainViewModel.MENU_MIEDIA_ARTIST,false),
       new ItemData($r('app.string.album'), $r('app.media.llq'),MainViewModel.MENU_MIEDIA_ALBUM,false),
-      new ItemData($r('app.string.user_center'), { type: 'symbol', value: $r('sys.symbol.person') }, MainViewModel.MENU_USER, false),
+      // new ItemData($r('app.string.user_center'), { type: 'symbol', value: $r('sys.symbol.person') }, MainViewModel.MENU_USER, false),
       new ItemData($r('app.string.file_scan'), { type: 'symbol', value: $r('sys.symbol.doc_text_badge_magnifyingglass') },MainViewModel.MENU_FILE_SCAN,false),
 
       new ItemData($r('app.string.music_charts'), { type: 'symbol', value: $r('sys.symbol.doc_plaintext') },MainViewModel.MENU_CHARTS,false),
 
-      new ItemData($r('app.string.setting'),  { type: 'symbol', value: $r('sys.symbol.gearshape') },MainViewModel.MENU_SETTING,false),
+      // new ItemData($r('app.string.setting'),  { type: 'symbol', value: $r('sys.symbol.gearshape') },MainViewModel.MENU_SETTING,false),
       new ItemData($r('app.string.about_me'), { type: 'symbol', value: $r('sys.symbol.info_shield') },MainViewModel.MENU_ABOUT,false),
 
     ];

BIN
entry/src/main/resources/base/media/app_loading0.png


BIN
entry/src/main/resources/base/media/app_loading1.png


BIN
entry/src/main/resources/base/media/app_loading2.png


BIN
entry/src/main/resources/base/media/app_loading3.png