Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ scripts/*

# Local release artifacts
release-symbols/

# Audio Haptics SDK lives in the sibling moonlight-audio-haptics repository.
/audio-haptics-sdk/
26 changes: 26 additions & 0 deletions cmake/ResolveMoonlightAudioHaptics.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Resolve the standalone Apache-2.0 audio-haptics SDK for host-side tools and
# HarmonyOS integration. CI should set AUDIO_HAPTICS_SDK_DIR to an immutable
# checkout; local sibling checkout is only a developer convenience.

set(AUDIO_HAPTICS_SDK_DIR "" CACHE PATH
"Path to the standalone moonlight-audio-haptics checkout")

function(resolve_moonlight_audio_haptics output_var repository_root)
set(_sdk_path "${AUDIO_HAPTICS_SDK_DIR}")
if(_sdk_path STREQUAL "" AND DEFINED ENV{AUDIO_HAPTICS_SDK_DIR})
set(_sdk_path "$ENV{AUDIO_HAPTICS_SDK_DIR}")
endif()
if(_sdk_path STREQUAL "")
set(_sdk_path "${repository_root}/../moonlight-audio-haptics")
endif()

get_filename_component(_sdk_path "${_sdk_path}" ABSOLUTE)
if(NOT EXISTS "${_sdk_path}/CMakeLists.txt")
message(FATAL_ERROR
"moonlight-audio-haptics not found at ${_sdk_path}. "
"Clone https://github.com/AlkaidLab/moonlight-audio-haptics and "
"set AUDIO_HAPTICS_SDK_DIR to its root.")
endif()

set(${output_var} "${_sdk_path}" PARENT_SCOPE)
endfunction()
698 changes: 698 additions & 0 deletions docs/AUDIO_HAPTICS_ANDROID_DEVICE_BENCHMARK.md

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions docs/AUDIO_HAPTICS_P0_BASELINE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 音频驱动触觉 P0 基线报告

> 状态:P0 完成
> 日期:2026-07-15
> 基线提交:`a9092a5` 加当前工作区 P0 工具
> 对应方案:`AUDIO_HAPTICS_SDK_IMPLEMENTATION_PLAN.md` 第 9、10 节

## 1. P0 交付结果

已建立可重复运行的主机评测链路:

- Release CMake/Ninja host runner,同时编译当前 aubio 最小集和当前 `SpectralOnsetDetector`。
- PCM16 RIFF WAV 读取、onset 事件 CSV、单文件 JSON 汇总。
- 基于标签的 precision、recall、F1 和输出时间误差。
- 不含初始化/WAV I/O 的 `ProcessFrame` P50/P95/P99/Max 与 realtime factor。
- 六类确定性合成 WAV、标签和 manifest 生成器。
- 一键构建、生成数据、逐用例执行和聚合 CSV/JSON 的脚本。
- CTest fixture 生成与端到端 runner smoke test。

P0 工具仍属于当前 GPLv3 宿主仓库,因为它需要链接 aubio 基线。它不进入后续 Apache-2.0 SDK。

## 2. 复现方式

```bash
python tools/audio_haptics_eval/run_baseline.py
```

输出目录:

```text
tools/audio_haptics_eval/out/
├─ baseline.csv
├─ baseline.json
└─ <case_id>/
├─ events.csv
└─ summary.json
```

运行 smoke test:

```bash
cmake -S tools/audio_haptics_eval \
-B tools/audio_haptics_eval/build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release
cmake --build tools/audio_haptics_eval/build
ctest --test-dir tools/audio_haptics_eval/build --output-on-failure
```

## 3. 测试环境

| 项目 | 值 |
|---|---|
| OS | Windows NT 10.0.26200.0 |
| CPU | AMD Ryzen 7 7840HS,8C/16T |
| Compiler | MinGW-w64 GCC 13.2.0 |
| CMake | 3.29.2 |
| Ninja | 1.12.0 |
| Python | 3.12.10 |
| Build | Release |
| Audio | PCM16、48 kHz、mono/stereo |
| Hop | 240 samples / 5 ms |
| Benchmark | 3 warmup + 20 measured runs |
| Label match window | ±50 ms |

主机 benchmark 只用于算法迭代回归,不能替代 HarmonyOS/Android 目标真机数据。

## 4. 初始数据集

| Case | Labels | 目的 |
|---|---:|---|
| `impulse_train_mono` | 5 | 宽带瞬态 recall 与输出时间 |
| `kick_train_stereo` | 6 | 低频冲击、重复触发和立体声输入 |
| `antiphase_impulses_stereo` | 4 | 暴露波形下混导致的反相抵消 |
| `silence_then_hit_mono` | 1 | 数字静音后的状态恢复 |
| `steady_tone_stereo` | 0 | 持续低频负样本误触发 |
| `speech_like_mono` | 0 | 语音类负样本误触发 |

所有 fixture 由固定代码和固定随机种子生成,不提交 WAV 二进制。该集合是工程 smoke baseline,不替代上线前需要的授权真实游戏/音乐/语音数据集。

## 5. 首轮结果

### 5.1 事件质量与输出时间

| Case | Backend | Events | F1 | Median abs error | P95 abs error |
|---|---|---:|---:|---:|---:|
| impulse train | aubio | 5 | 1.000 | 15 ms | 15 ms |
| impulse train | native current | 2 | 0.571 | 30 ms | 30 ms |
| kick train | aubio | 12 | 0.667 | 15 ms | 15 ms |
| kick train | native current | 12 | 0.667 | 35 ms | 35 ms |
| antiphase | aubio | 0 | 0.000 | — | — |
| antiphase | native current | 0 | 0.000 | — | — |
| silence then hit | aubio | 1 | 1.000 | 15 ms | 15 ms |
| silence then hit | native current | 1 | 1.000 | 30 ms | 30 ms |
| steady tone(negative) | aubio | 29 false events | 0.000 | — | — |
| steady tone(negative) | native current | 3 false events | 0.000 | — | — |
| speech-like(negative) | aubio | 9 false events | 0.000 | — | — |
| speech-like(negative) | native current | 7 false events | 0.000 | — | — |

说明:timestamp 记录 detector 在 hop 末尾返回 onset 的时刻,因此包含 detector 本身的前视/确认延迟。Kick burst 的余振被两个后端各自重复识别一次,导致每个标注约产生两个事件。

### 5.2 主机性能

| Backend | 跨全部 fixture 的最高 call P99 | 最低 realtime factor |
|---|---:|---:|
| aubio | 66.5 µs | 93.6x |
| native current | 37.6 µs | 179.9x |

两者在该主机上都低于每 5 ms 音频块 0.5 ms 的 P99 预算,native current 约有明显余量。重复测量时观察到 Windows 调度造成的少数毫秒级 `call_max` 离群点,因此 P0 不用单次 max 作为算法 gate;移动端必须重新测 P99 和音频 underrun。

## 6. 基线结论

1. **评测基础设施已可用**:同一 PCM 可以稳定输出 aubio/native 事件、标签分数和调用耗时。
2. **现有 native 不能直接替换 aubio**:普通宽带 impulse 只召回 2/5,说明当前白化/历史谱或 picker 状态存在明显问题。
3. **25 ms 前视问题可被观测**:native current 的输出中位误差为 30~35 ms,aubio 为 15 ms;P2 必须将未来前视压到 0~1 hop。
4. **反相抵消已经被固定用例捕获**:两个后端在 antiphase case 都为 0/4,P2 的 channel-aware magnitude/energy fusion 必须把该用例提升到 4/4。
5. **持续声和语音必须进入产品门控**:aubio 在两个负样本上产生 38 次事件,native current 产生 10 次;仅以“接近 aubio”为目标不足以获得拟真触感。
6. **低频 burst 会产生重复触发**:需要结合 refractory、持续声判定或 transient/body 分离,不能只靠 80 ms MinIOI。

## 7. P1/P2 的明确输入条件

后续实现不得降低以下可观测性:

- 保持 `events.csv` 与 `summary.json` schema version,破坏性修改必须升版本。
- 新 Core 后端必须接入同一个 runner,不能建立不可比较的另一套工具。
- `antiphase_impulses_stereo` 目标 recall 为 1.0。
- `impulse_train_mono` 目标 recall 不低于 aubio 的 1.0,输出 median error 目标不超过 10 ms。
- 两个 negative case 的 false events 必须明显低于当前 aubio 基线,并在真实数据集上复验。
- 性能结果必须同时报告 host 与至少两类目标移动设备,禁止用本报告的桌面数据代替真机 gate。

## 8. 下一步

进入 P1:建立 Apache-2.0 SDK 目录、公共 C ABI 与 `HapticFrame`,同时让新 Core 能作为第三个 backend 接入本评测器。aubio 与 `native_current` 仅保留为基线,不迁入 SDK。
142 changes: 142 additions & 0 deletions docs/AUDIO_HAPTICS_P1_SDK_FOUNDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# 音频驱动触觉 P1 SDK 基础报告

> 状态:P1 完成
> 日期:2026-07-15
> SDK 版本:0.1.0
> ABI 版本:1
> 许可证:Apache License 2.0
> 迁移说明:本文记录 P1 当时的仓库内路径;SDK 已于 2026-07-17 迁移到 [独立仓库](https://github.com/AlkaidLab/moonlight-audio-haptics),当前发布基线为 `v0.5.14`。

## 1. P1 交付结果

已在仓库顶层建立独立 `audio-haptics-sdk`:

- Apache-2.0 `LICENSE`、SPDX 文件头和第三方依赖清单。
- 可被 C11、C++17、HarmonyOS BiSheng 和 Android NDK 使用的公共 C ABI。
- 80-byte 固定 `AhHapticFrame` v1 中间表示。
- Engine 创建、配置更新、输入容量计算、PCM 消费、reset 和 destroy 生命周期。
- 初始化后处理路径零 heap allocation、无锁、无回调、无日志和无平台 API。
- C API 行为测试、C++ ABI layout 测试和许可证边界测试。
- P0 evaluator 的 `sdk_core_p1` 第三后端。
- HarmonyOS `nativelib` 静态链接与 arm64-v8a/x86_64 构建验证。

P1 Core 故意不产生触觉事件。它只验证 SDK 边界和生命周期;共享特征、PCEN 与因果 onset 在 P2 实现。

## 2. 公共 ABI

公共入口位于:

```text
audio-haptics-sdk/include/moonlight_haptics/
├─ audio_haptics.h
└─ version.h
```

v1 提供:

```text
ah_config_init
ah_create
ah_update_config
ah_get_max_output_frames
ah_process_i16
ah_reset
ah_destroy
ah_get_abi_version
ah_get_version_string
ah_status_string
```

关键决定:

- `AhStatus`、`AhScene`、`AhFrameFlags` 均为明确的 32-bit 类型,不依赖编译器 enum 尺寸。
- `AhHapticFrame` v1 固定 80 bytes,时间戳偏移 8,幅度区偏移 16,场景偏移 44,reserved 偏移 48。
- IR 是数组输出,因此不能在后续版本简单追加结构字段并改变元素步长。小版本消耗 8 个 reserved 字段;更大结构必须新增 API/ABI。
- `AhConfig` 的 sample rate/channel count 不允许运行时变化,变化返回 `AH_STATUS_RECREATE_REQUIRED`。
- 一次 PCM 可能跨越多个 hop;调用方用 `ah_get_max_output_frames()` 预分配固定输出数组。
- 输出空间不足时不消费 PCM,便于宿主无损重试。

## 3. 线程与内存模型

- `ah_create()` 是唯一允许的 Engine heap allocation。
- `ah_process_i16()` 由单一音频线程调用。
- sensitivity、gain、scene 和 feature flags 使用 lock-free 32-bit atomic 存储,可由控制线程更新。
- `reset/destroy` 由宿主保证不与 `process` 并发。
- Core 不创建线程,也不依赖 N-API、JNI、HarmonyOS 或 Android 头文件。

P2 增加 ring buffer/FFT scratch 时必须在 `ah_create()` 一次性完成固定容量分配,并保持处理路径零分配。

## 4. 构建接入

### 4.1 独立 host

```bash
cmake -S audio-haptics-sdk \
-B audio-haptics-sdk/build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release
cmake --build audio-haptics-sdk/build
ctest --test-dir audio-haptics-sdk/build --output-on-failure
```

### 4.2 P0 evaluator

Evaluator 通过 `moonlight::haptics` 链接 SDK,并提供 `sdk_core_p1` backend:

```bash
python tools/audio_haptics_eval/run_baseline.py --backend all
```

P1 backend 在所有 fixture 上应为零事件。这是预期行为,不是算法分数;P2 将在该 backend 内产生第一批可比较 IR。

### 4.3 HarmonyOS

`nativelib/src/main/cpp/CMakeLists.txt` 通过 `add_subdirectory` 和静态链接消费同一 Core。生产 `BassEnergyAnalyzer` 未切换,也没有新增运行时回调。

验证产物:

| ABI | Core static library | 本次 Debug 大小 |
|---|---|---:|
| arm64-v8a | `libmoonlight_haptics_core.a` | 66,732 bytes |
| x86_64 | `libmoonlight_haptics_core.a` | 65,788 bytes |

`nativelib.har` 已成功生成;大小 12,377,581 bytes。静态库目前没有被生产代码引用,最终链接器可移除未使用对象,因此该数字不代表发布包净增量。

## 5. 验证结果

| 验证 | 结果 |
|---|---|
| SDK Release configure/build | 通过 |
| C11 API lifecycle test | 通过 |
| C++17 ABI/layout test | 通过 |
| Apache-2.0 license boundary test | 通过 |
| P0 evaluator build | 通过 |
| Evaluator fixture + end-to-end smoke | 通过 |
| 六类 fixture 的 `sdk_core_p1` backend | 通过,预期零事件 |
| HarmonyOS arm64-v8a native build | 通过 |
| HarmonyOS x86_64 native build | 通过 |
| HarmonyOS debug HAR package | 通过,24.752 s |

Harmony 构建只有项目已有的 N-API `.d.ts` 验证警告,与 SDK Core 无关。

## 6. 许可证边界

- SDK 目录内不包含或链接 aubio/GPL 源码。
- SDK P1 没有第三方 runtime 源码或二进制依赖。
- CMake 测试会扫描 SDK C/C++/Python/CMake 文件的 Apache-2.0 SPDX,并拒绝 aubio include、GPL/AGPL 标记。
- P0 evaluator 继续为 GPL,因为它同时链接 aubio 基线;其中的 adapter 只通过 SDK 公共 C ABI 调用 Core。
- 引入 FFT、模型或 runtime 前必须更新 `THIRD_PARTY_NOTICES.md` 并通过许可证准入。

## 7. P2 入口条件

P2 在不改变 ABI v1 的前提下实现:

1. 固定容量多声道 PCM ring buffer。
2. channel-aware 能量融合,先让 antiphase fixture 从 0/4 提升到 4/4。
3. 共享 Hann/STFT 特征层和预分配 scratch。
4. PCEN/自适应归一化。
5. 0~1 hop 因果 onset picker。
6. transient amplitude、sharpness、confidence 和 timestamp 输出。
7. `sdk_core_p1` backend 更名为稳定的 `sdk_core`,开始纳入 P0 质量对比。

P2 完成前不接管 HarmonyOS 正式振动输出。
Loading
Loading