diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b2304c1..2ea1de0 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -21,12 +21,14 @@ jobs: - run: zsh scripts/test-plugin-macos.sh - run: | python3 tools/plugins/fixture.py - build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/staff-note build/plugin-fixture.json - MIUCS_FORCE_CPU=1 build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/staff-note build/plugin-fixture.json + build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/pixel-keyboard build/plugin-fixture.json + MIUCS_FORCE_CPU=1 build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/pixel-keyboard build/plugin-fixture.json build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/niulai-keyboard build/plugin-fixture.json MIUCS_FORCE_CPU=1 build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/niulai-keyboard build/plugin-fixture.json build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/muffin-puppies build/plugin-fixture.json MIUCS_FORCE_CPU=1 build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/muffin-puppies build/plugin-fixture.json + build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/heiji-kazuha build/plugin-fixture.json + MIUCS_FORCE_CPU=1 build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/heiji-kazuha build/plugin-fixture.json build/plugins/plugin-benchmark plugins build/plugin-fixture.json > build/plugin-benchmark.json - uses: actions/upload-artifact@v4 if: always() @@ -68,6 +70,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'Muffin lifecycle validation failed' } python tests/plugins/muffin_replay.py if ($LASTEXITCODE -ne 0) { throw 'Muffin input replay failed' } + python tools/plugins/validate.py plugins/heiji-kazuha + if ($LASTEXITCODE -ne 0) { throw 'Heiji Kazuha lifecycle validation failed' } + python tests/plugins/heiji_kazuha_replay.py + if ($LASTEXITCODE -ne 0) { throw 'Heiji Kazuha input replay failed' } python tools/plugins/preview.py if ($LASTEXITCODE -ne 0) { throw 'CPU preview failed' } build/plugins/Release/plugin-surface-probe.exe build/plugins/Release/miucs-plugin-worker.exe plugins build/plugin-fixture.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a9dd41..36d51cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ ## 平台支持 -| 平台 | 音频采集 / 恢复 | 分析后端 | 铅笔飞鸟绘制 | 五线谱音符绘制 | 应用外壳 | +| 平台 | 音频采集 / 恢复 | 分析后端 | 四个插件绘制 | 插件适配 | 应用外壳 | | --- | --- | --- | --- | --- | --- | | macOS 14+ / Apple Silicon | 已实现,含自动恢复 | Swift 分析 + Accelerate FFT | Lua/Skia,共用五种笔色 | Lua/Skia,共用五种笔色 | 已实现 | | Windows 10/11 x64 | WASAPI 已实现;实机待验 | C# FFT/混音旋律/节拍;合成测试 | Lua/Skia;实机待验 | Lua/Skia;实机待验 | 托盘/透明窗口已实现;实机待验 | @@ -77,11 +77,11 @@ python3 scripts/benchmark-pop-melody.py --prepare --adc-dataset build/adc2004 -- sh scripts/build-plugins.sh sh scripts/test-plugins.sh # C++ 运行库、固定数值回放、预览、传输与生命周期 zsh scripts/test-plugin-macos.sh # macOS 插件选择、独立 worker、热重载和托盘 -python3 tools/plugins/validate.py plugins/staff-note +python3 tools/plugins/validate.py plugins/pixel-keyboard python3 tools/plugins/preview.py ``` -数值回放使用 `tests/plugins/reference.json.gz` 中的固定参考,覆盖三个原有插件各 600 帧。参考来自移除前的模拟器,输入哈希防止测试数据无意漂移;维护说明见 [测试基线](tests/plugins/README.md)。性能测试使用 `build/plugins/plugin-benchmark`,持续内存检查使用 `tests/plugins/soak.py`。 +数值回放使用 `tests/plugins/reference.json.gz` 中的固定参考,覆盖奶牛猫的 600 帧头部与尾巴动画;麦芬、牛来和平次与和叶由各自规则回放验证。参考来自移除前的模拟器,输入哈希防止测试数据无意漂移;维护说明见 [测试基线](tests/plugins/README.md)。性能测试使用 `build/plugins/plugin-benchmark`,持续内存检查使用 `tests/plugins/soak.py`。 ## Windows 接入 diff --git a/README.md b/README.md index 57e5a7f..8842a9d 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,60 @@ -# Miucs · 桌面音乐可视化 +# Miucs · 奶牛猫桌面伙伴 -**让音乐自己飞。** +**打字时伸爪,听音乐时摇尾。** -把音乐和打字变成桌面上的动画:随旋律飞行的小鸟、随节奏摆动的音符,以及跟着按键敲击的像素猫。画面透明、默认鼠标穿透,不抢键盘焦点。 +奶牛猫是 Miucs 的默认桌面伙伴:黑白猫爪跟随按键,猫头随打字轻晃,尾巴响应音乐节拍。画面透明、默认鼠标穿透,不抢键盘焦点。 `macOS 14+ · Apple Silicon · Swift 6` · `Windows 10/11 · 实机待验` -![像素猫键盘:双爪跟随按键,猫头随打字摇动,尾巴随音乐摆动](docs/assets/miucs-pixel-keyboard.gif) +![奶牛猫:猫爪跟随打字,猫头与尾巴响应输入](docs/assets/plugins/pixel-keyboard.gif) -> 合成输入驱动原生绘制器生成的演示,未录制真实输入或桌面;GIF 无声。 +> 预览使用合成音乐与按键输入,经正式 Lua/Skia 绘制器生成;GIF 无声,不是桌面录屏。 -## 三种可视化 +## 四个桌面伙伴 -[查看工作台生成的全部插件 GIF 预览](docs/plugin-previews.md),包含「牛来」和「麦芬与线条小狗」。 - -| 模式 | 效果 | 详细说明 | +| 插件 | 效果 | 详细说明 | | --- | --- | --- | -| 像素音乐键盘 | 猫爪敲击真实物理键位,猫头随打字速度摇动,尾巴响应音乐节拍 | [像素键盘](docs/pixel-keyboard.md) | -| 五线谱音符 | 主旋律驱动音符与轨迹,节拍展开轻弧,按键散出星星 | [五线谱](docs/staff-note.md) | -| 铅笔飞鸟 | 音高决定飞行高度,音量与起音带动振翅,管道自动避让 | [飞鸟](docs/sound-flight.md) | +| **奶牛猫 · 默认主打** | 黑白猫爪敲键、猫头轻晃、尾巴随节拍摆动 | [奶牛猫](docs/pixel-keyboard.md) | +| 麦芬 | 两只线条小狗随打字跳跃,麦芬随节拍吹泡泡 | [麦芬](plugins/muffin-puppies/README.md) | +| 牛来 | 牛蹄响应按键,上方角色随音乐和打字运动 | [牛来](plugins/niulai-keyboard/README.md) | +| 平次与和叶 | 平次随打字挥木剑,和叶随节拍转身 | [平次与和叶](plugins/heiji-kazuha/README.md) | -音频和键盘可以同时驱动画面,也能独立使用。每种模式分别记住位置和大小,切换时不重启输入采集。仅使用 Lua/Skia 插件,也可以添加自己编写的 Lua 可视化。 +[查看四个插件的动态预览](docs/plugin-previews.md)。首次启动默认选择奶牛猫;之后记住你的选择。每个插件分别保存位置和大小,切换不重启输入采集。 ## 构建与启动 ### macOS -需要 Apple Silicon、macOS 14+、支持 Swift 6 的 Xcode 或 Command Line Tools、Python 3.10+ 和 CMake 3.24+;首次创建本地开发签名需要 OpenSSL 3。在仓库根目录执行: +需要 Apple Silicon、macOS 14+、Swift 6、Python 3.10+、CMake 3.24+;首次本地开发签名需要 OpenSSL 3。 ```sh zsh build.sh open dist/Miucs.app ``` -首次运行按提示授权系统音频捕获;需要打字效果时,授予「输入监控」权限。菜单中的「键盘监听」可查看状态。休眠唤醒或音频中断后会自动重连,也可手动选择「重新连接音频」。 +首次运行按提示授权系统音频捕获;要让猫爪响应打字,授予「输入监控」权限。音频和键盘可以同时使用,也能独立运行。系统休眠或音频中断后自动恢复,也可在菜单选择「重新连接音频」。更新后先退出旧版,再打开新构建。 ### Windows -已有 .NET 8/WPF、WASAPI 实现,实机采集、绘制及多屏验收尚未完成。构建步骤见 [Windows README](apps/windows/README.md),支持边界见 [验收表](docs/windows-acceptance.md)。 +提供 .NET 8/WPF、WASAPI 实现。构建方法见 [Windows README](apps/windows/README.md),实机验证范围见 [验收表](docs/windows-acceptance.md)。 ## 常用操作 -从菜单栏或托盘打开菜单: - -| 操作 | 用途 | -| --- | --- | -| 可视化 | 切换当前画面 | -| 显示 / 隐藏拾音器 | 控制悬浮画面的显示 | -| 画面大小 | 调整缩放比例 | -| 线条颜色 | 选择五种笔色;像素键盘保留固定配色 | -| 画面不透明度 | 10%~100%,即时生效并自动保存 | -| 调整位置与大小… | 拖动画面移动、拖动右下角缩放,完成后恢复鼠标穿透 | - -也可以将鼠标停留在画面上 3 秒,点击出现的小锁来解锁或锁定。菜单栏猫图标属于应用本身,不随可视化插件切换。 - -## 自己制作插件 +- **可视化**:切换四个桌面伙伴,奶牛猫排在首位。 +- **显示 / 隐藏画面**:控制浮窗显示。 +- **画面大小、不透明度**:等比缩放,调整 10%~100% 不透明度。 +- **调整位置与大小**:拖动画面或右下角缩放,完成后恢复鼠标穿透;也可悬停 3 秒点击小锁。 +- **托盘猫摇头**:应用自己的猫图标,独立于当前插件。 -插件使用 **Lua 5.4 + Skia**,一个文件夹对应一个可视化。复制模板后,编写场景与动画,放入用户插件目录即可加载,无需重新编译应用。 +## 插件与隐私 -所有构建均包含插件运行库和内置插件。首次构建会下载并校验 Lua、JSON 库和 Skia 依赖: +应用内置四个 Lua 5.4/Skia 插件,也可安装自己编写的插件。首次构建自动下载并校验运行库依赖。开发和调试见 [插件开发指南](docs/plugin-development.md) 与 [插件工作台](docs/plugin-lab.md)。 -```sh -zsh build.sh -open dist/Miucs.app -``` - -更新后请先退出已运行的 Miucs,再打开新版本。应用只支持插件可视化;运行库缺失时会显示错误。 - -**[插件开发指南 →](docs/plugin-development.md)**:包含完整示例、双端安装步骤、输入数据、热重载、验证预览和常见问题。 - -## 本地处理 - -系统音频在本机分析,不依赖网络服务或外部模型;应用不保存真实音频、屏幕画面或输入文字。键盘效果使用物理键位,不受输入法影响。 - -主旋律是对混音中突出音高的近似估计,复杂伴奏可能影响结果;音符用于可视化,不生成完整乐谱。算法与限制见[混音主旋律说明](docs/predominant-mixture.md)。 +音频与物理按键只在本机分析,不依赖网络模型;应用不保存真实音频、屏幕或输入文字。 ## 开发文档 -- [贡献指南与测试命令](CONTRIBUTING.md) -- [插件开发指南](docs/plugin-development.md) · [插件 API 契约](contracts/plugins/README.md) -- [架构与依赖边界](docs/architecture.md) +- [贡献指南与测试](CONTRIBUTING.md) · [架构与依赖边界](docs/architecture.md) +- [插件契约](contracts/plugins/README.md) · [可视化契约](contracts/visualization/README.md) - [音频数据](contracts/audio-features/README.md) · [键盘数据](contracts/keyboard-input/README.md) - [外观设置](docs/appearance.md) · [验证记录](docs/validation/README.md) - -插件版另带 **「牛来」**,详见[牛来说明](plugins/niulai-keyboard/README.md)。 - -插件版另带 **「麦芬与线条小狗」**,详见[麦芬皮肤说明](plugins/muffin-puppies/README.md)。皮肤采用断续手绘键盘,两只小狗随按键播放模型绘制的跳跃帧,麦芬随音乐节拍吹泡泡。 diff --git a/apps/macos/PluginVisualizations.swift b/apps/macos/PluginVisualizations.swift index 027ec12..b5ceb0e 100644 --- a/apps/macos/PluginVisualizations.swift +++ b/apps/macos/PluginVisualizations.swift @@ -3,7 +3,7 @@ import Foundation /// The catalog is the only source of visualization sessions. @MainActor enum PluginVisualizations { - static let defaultID = "pencil-flight" + static let defaultID = "pixel-keyboard" static func registry() -> VisualizationRegistry { let registry = VisualizationRegistry() PluginHost.shared.register(in: registry) diff --git a/apps/macos/VisualizationAppearanceStore.swift b/apps/macos/VisualizationAppearanceStore.swift index 4dccd4f..51830bc 100644 --- a/apps/macos/VisualizationAppearanceStore.swift +++ b/apps/macos/VisualizationAppearanceStore.swift @@ -14,17 +14,11 @@ final class VisualizationAppearanceStore: ObservableObject { private let preferences: UserDefaults var appearance: VisualizationAppearance { selection.appearance } - // Decode only the saved ink needed to migrate existing installations. - private struct PreviousPalette: Decodable { let bird: VisualizationColor } - init(preferences: UserDefaults = .standard) { self.preferences = preferences transparency = min(90, max(0, preferences.object(forKey: Self.transparencyKey) as? Int ?? 0)) if let raw = preferences.string(forKey: Self.preferencesKey) { selection = VisualizationInk(rawValue: raw) ?? .graphite - } else if let data = preferences.data(forKey: "MiucsPencilPalette.v1"), - let legacy = try? JSONDecoder().decode(PreviousPalette.self, from: data) { - selection = VisualizationInk.nearest(to: legacy.bird) } else { selection = .graphite } } // Called only by a normal launch, so offline diagnostics remain read-only. diff --git a/apps/macos/VisualizerPanel.swift b/apps/macos/VisualizerPanel.swift index c364363..65c6696 100644 --- a/apps/macos/VisualizerPanel.swift +++ b/apps/macos/VisualizerPanel.swift @@ -5,10 +5,10 @@ import AppKit final class VisualizerPanel: NSPanel, NSWindowDelegate { static let selectionPreferencesKey = "MiucsSelectedVisualization.v1" static func framePreferencesKey(for id: String) -> String { - id == "pencil-flight" ? "MiucsPencilFlightFrame" : "MiucsVisualizationFrame.v1.\(id)" + "MiucsVisualizationFrame.v1.\(id)" } private static func autosaveName(for id: String) -> String { - id == "pencil-flight" ? "MiucsPencilFlight" : "MiucsVisualization.\(id)" + "MiucsVisualization.\(id)" } let visualizationHost: VisualizationHostView private let preferences: UserDefaults @@ -33,7 +33,7 @@ final class VisualizerPanel: NSPanel, NSWindowDelegate { preferences: UserDefaults = .standard, persistsFrame: Bool = true, keyboardSource: (any KeyboardFeatureSource)? = nil, monitorsHover: Bool = true) { let requestedID = selectedID ?? preferences.string(forKey: Self.selectionPreferencesKey) - let initial = requestedID.flatMap { registry.descriptor(id: $0) } ?? registry.descriptors.first! + let initial = requestedID.flatMap { registry.descriptor(id: $0) } ?? registry.descriptor(id: PluginVisualizations.defaultID) ?? registry.descriptors.first! descriptor = initial visualizationHost = VisualizationHostView(source: source, registry: registry, selectedID: initial.id, keyboardSource: keyboardSource) @@ -94,7 +94,7 @@ final class VisualizerPanel: NSPanel, NSWindowDelegate { } private func updateTitle() { - title = descriptor.id == "pencil-flight" ? "Miucs · Pencil Flight" : "Miucs · \(descriptor.name)" + title = "Miucs · \(descriptor.name)" } private func restoreGeometry(fallbackTopLeft: NSPoint?) { @@ -112,8 +112,7 @@ final class VisualizerPanel: NSPanel, NSWindowDelegate { setFrame(NSRect(x: topLeft.x, y: topLeft.y - layout.height, width: layout.width, height: layout.height), display: false) } - let legacyWidth = descriptor.id == "pencil-flight" ? preferences.object(forKey: "MiucsPencilFlightWidth") as? Double : nil - let width: Double = saved.map { Double($0.width) } ?? (restored ? Double(frame.width) : legacyWidth ?? layout.width) + let width: Double = saved.map { Double($0.width) } ?? (restored ? Double(frame.width) : layout.width) setDrawingWidth(width, persist: false) visualizationHost.updateViewport() if persistsFrame { setFrameAutosaveName(Self.autosaveName(for: descriptor.id)) } @@ -151,7 +150,6 @@ final class VisualizerPanel: NSPanel, NSWindowDelegate { frame.width.isFinite, frame.height.isFinite, frame.width > 0, frame.height > 0 else { return } preferences.set(["x": frame.minX, "y": frame.minY, "width": frame.width, "height": frame.height], forKey: Self.framePreferencesKey(for: descriptor.id)) - if descriptor.id == "pencil-flight" { preferences.set(frame.width, forKey: "MiucsPencilFlightWidth") } preferences.set(descriptor.id, forKey: Self.selectionPreferencesKey) if persistsFrame { saveFrame(usingName: Self.autosaveName(for: descriptor.id)) } } diff --git a/apps/windows/Core/Preferences.cs b/apps/windows/Core/Preferences.cs index 5d47296..c61030b 100644 --- a/apps/windows/Core/Preferences.cs +++ b/apps/windows/Core/Preferences.cs @@ -27,7 +27,7 @@ public static WindowFrame Fit(WindowFrame requested, WindowFrame workArea, doubl public sealed class Preferences { - public string Visualization { get; set; } = "pencil-flight"; + public string Visualization { get; set; } = "pixel-keyboard"; public string Color { get; set; } = "graphite"; private int transparency; public int Transparency { get => transparency; set => transparency = Math.Clamp(value, 0, 90); } @@ -42,7 +42,7 @@ public static Preferences Load(string path) var result = JsonSerializer.Deserialize(File.ReadAllText(path)); if (result == null) return new(); result.Frames ??= new(); - if (string.IsNullOrWhiteSpace(result.Visualization) || result.Visualization.Length > 128) result.Visualization = "pencil-flight"; + if (string.IsNullOrWhiteSpace(result.Visualization) || result.Visualization.Length > 128) result.Visualization = "pixel-keyboard"; result.Color = Palette.ContainsKey(result.Color ?? "") ? result.Color! : "graphite"; foreach (var key in new List(result.Frames.Keys)) if (result.Frames[key] == null || !result.Frames[key].IsValid) result.Frames.Remove(key); diff --git a/apps/windows/DesktopHost.cs b/apps/windows/DesktopHost.cs index 028abc4..6180c63 100644 --- a/apps/windows/DesktopHost.cs +++ b/apps/windows/DesktopHost.cs @@ -113,7 +113,7 @@ private void Dispatch(Action action) private void SwitchVisualization(string id) { if (!pluginHost.Views.ContainsKey(id)) - id=pluginHost.Views.Keys.FirstOrDefault() ?? ""; + id=pluginHost.Views.ContainsKey("pixel-keyboard") ? "pixel-keyboard" : pluginHost.Views.Keys.FirstOrDefault() ?? ""; if (view != null && view.Id == id) return; WindowFrame? previous = view == null ? null : CurrentFrame(); hoverLock?.Reset(); diff --git a/apps/windows/README.md b/apps/windows/README.md index 24d91b0..b3e6a80 100644 --- a/apps/windows/README.md +++ b/apps/windows/README.md @@ -8,7 +8,7 @@ 解压 `Miucs-windows-x64.zip`,保留整个目录,运行 `Miucs.exe`。自包含版本无需单独安装 .NET,也不需要管理员权限。退出后可直接删除目录。无安装器、无签名、无开机自启、无注册表持久化;未签名的新程序可能显示 Windows 信任提示,应先核对来源及 SHA-256,不要关闭系统安全设置。 -启动后在通知区域寻找 Miucs 图标(可能位于折叠区)。默认显示飞鸟,捕获**当前默认多媒体播放设备**的系统混音,不使用麦克风。播放音乐后自动驱动画面。WASAPI 空闲设备可能没有 PCM;此时不伪造静音帧,也不因为没有音乐不断重连。 +启动后在通知区域寻找 Miucs 图标(可能位于折叠区)。默认显示奶牛猫,捕获**当前默认多媒体播放设备**的系统混音,不使用麦克风。播放音乐后自动驱动画面。WASAPI 空闲设备可能没有 PCM;此时不伪造静音帧,也不因为没有音乐不断重连。 - **可视化**:仅加载插件目录中的画面;运行库缺失时显示启动错误。切换不重启两路输入。 - **托盘猫摇头**:所有模式统一显示同一只像素猫;托盘动画独立于插件,开启键盘输入后随打字速度摇头,停下后回正。隐藏画面仍可响应,切换模式不换猫、不重启动作。可从菜单关闭托盘动画;锁屏/休眠或 Windows 关闭界面动画时保持静止。不会因此自动开启键盘监听。 diff --git a/apps/windows/WindowsSmokeTests.cs b/apps/windows/WindowsSmokeTests.cs index ba3f151..b305ed6 100644 --- a/apps/windows/WindowsSmokeTests.cs +++ b/apps/windows/WindowsSmokeTests.cs @@ -24,7 +24,7 @@ public static void Run(string output) private static void VerifyPluginRendering() { using var host = new PluginHost(new AudioChannel(), new KeyboardChannel()); - foreach (string id in new[] { "pencil-flight", "staff-note", "pixel-keyboard" }) + foreach (string id in new[] { "pixel-keyboard", "muffin-puppies", "niulai-keyboard", "heiji-kazuha" }) { if (!host.Views.ContainsKey(id)) throw new InvalidOperationException("Missing bundled plugin: " + id); using var view = new PluginView(host, id); diff --git a/contracts/audio-features/README.md b/contracts/audio-features/README.md index 542e076..17bf920 100644 --- a/contracts/audio-features/README.md +++ b/contracts/audio-features/README.md @@ -15,7 +15,7 @@ | `sequence` | 每个代次内从 1 递增的接收帧序号;跨代次不能比较 | | `audio.time`、`melody.time`、起音事件 `time` | PCM 分析窗口中心相对当前格式时间线的秒数;重连或输入格式改变可归零 | | `receivedAt` | 主机单调时钟秒数,macOS 为 `systemUptime`;不是 Unix 时间,也不能与 PCM 时间相减 | -| `frameAge` | 读取时 `max(0, 单调时钟 - receivedAt)`;无帧时缺失;飞鸟对超过 400ms 的帧按失联处理 | +| `frameAge` | 读取时 `max(0, 单调时钟 - receivedAt)`;无帧时缺失;插件按自身规则处理过期帧 | 无效 Optional 可省略或写 `null`,不能用 `0`、NaN、Infinity 替代。零音高不能表示“没有旋律”;零相关性表示有效的不相关。`isSilent` 描述已采集声音,`state` 描述连接;运行但没有 PCM、静音 PCM、权限不足是不同状态。 diff --git a/contracts/keyboard-input/README.md b/contracts/keyboard-input/README.md index d499513..a5f9da8 100644 --- a/contracts/keyboard-input/README.md +++ b/contracts/keyboard-input/README.md @@ -1,6 +1,6 @@ # 键盘输入契约 v1 -键盘和音频是同时运行的独立生产者,各自拥有权限、状态、代次和订阅游标。输入值类型在 `modules/keyboard/core/`,macOS 采集与恢复在 `modules/keyboard/platforms/macos/`。五线谱将物理按键映射为星爆,飞鸟仍使用键盘入口的默认空实现。 +键盘和音频是同时运行的独立生产者,各自拥有权限、状态、代次和订阅游标。输入值类型在 `modules/keyboard/core/`,macOS 采集与恢复在 `modules/keyboard/platforms/macos/`。内置插件将物理按键映射为猫爪、牛蹄、角色跳跃或挥剑。 ## 快照与事件 @@ -32,7 +32,7 @@ 订阅者各自维护游标,互不排空事件。新订阅读取当前状态,不补播订阅前的按键。代次变化清空按住键、修饰键、频率和待消费事件;旧采集的迟到回调被拒绝。捕获代码只写内存,不为每个按键创建 UI 任务。 -可视化控制器在同一屏幕刷新中先调用 `receive(keyboard:)`,再调用既有音频 `update(input:deltaTime:)`。两路不互斥;任一路未授权也会继续传递另一路。`receive(keyboard:)` 默认空实现,五线谱覆盖它保存当前键盘快照,在统一动画更新中消费。隐藏/切换模块不停止两个生产者,恢复时两路重新订阅,均不补播旧动作。 +可视化控制器在同一屏幕刷新中先调用 `receive(keyboard:)`,再调用既有音频 `update(input:deltaTime:)`。两路不互斥;任一路未授权也会继续传递另一路。插件宿主在统一更新中读取两路快照,并发送给当前 Lua worker。隐藏/切换模块不停止两个生产者,恢复时两路重新订阅,均不补播旧动作。 ## macOS 监听 diff --git a/contracts/visualization/README.md b/contracts/visualization/README.md index a2af4f5..c592e72 100644 --- a/contracts/visualization/README.md +++ b/contracts/visualization/README.md @@ -6,7 +6,7 @@ 窗口切换顺序为:保存旧位置和尺寸、暂停绘制、释放旧会话、更换插件视图、恢复目标视口、激活。选择当前 ID 不重建会话;选择未知 ID 不影响正在显示的会话。启动时优先使用已保存 ID,其次使用可用的内置插件或目录首项。空目录保留重新扫描入口。 -外壳负责颜色、透明度、窗口位置、缩放、鼠标穿透、悬停锁和持久化。插件拥有输入映射、场景与动画;不控制系统采集或窗口。旧飞鸟位置和颜色偏好仍可读取。 +外壳负责颜色、透明度、窗口位置、缩放、鼠标穿透、悬停锁和持久化。插件拥有输入映射、场景与动画;不控制系统采集或窗口。奶牛猫沿用 `pixel-keyboard` 标识;失效的插件选择优先回退到奶牛猫。 输入格式见 [音频契约](../audio-features/README.md) 和 [键盘契约](../keyboard-input/README.md)。音频与键盘使用独立的代次、序号及权限。宿主同帧传递两路快照和短事件,隐藏恢复、重连和切换不得补播过期事件。 diff --git a/docs/appearance.md b/docs/appearance.md index d7c772c..a33d1c8 100644 --- a/docs/appearance.md +++ b/docs/appearance.md @@ -1,29 +1,15 @@ -# 公共笔色与画面不透明度 +# 画面外观 -三种可视化与后续模块共用一份笔色。菜单栏「线条颜色」提供石墨灰、雾蓝、苔绿、赭橙、雾紫五种选择,带色点与当前选择标记。点选后立即生效,切换模块、隐藏恢复和重启均沿用同一选择。 +四个插件保留自己的角色、键盘和配色。菜单提供 10%~100% 不透明度,每档 10%,默认 100%;作用于整个插件画面,小锁、调整控件和状态提示保持可读。 -## 画面不透明度 +每个插件独立保存位置和大小。奶牛猫继续使用 `pixel-keyboard` 标识;改名不重置已有窗口设置。首次启动和失效的插件选择回退到奶牛猫,其他有效选择按原偏好恢复。 -macOS 和 Windows 的菜单提供 10%~100% 不透明度,每档 10%,默认 100% 保持原始效果,数值越小越透明。它作用于整幅可视化,包括像素键盘的屏幕、机身、猫头和猫爪,与模块本身的淡入淡出叠加;小锁、缩放控件与状态提示不淡化。 +宿主仍提供五种笔色并通过 `appearanceChanged` 传递给插件,供支持换色的自定义插件使用。内置角色图集不会被统一染色。 -macOS 通过外观存储的 `MiucsVisualizationTransparency.v1` 保存透明度整数百分比,应用菜单与状态栏菜单共用存储;界面不透明度为 `100 - 存储值`,兼容已有选择。宿主调整当前模块视图的 `alphaValue`,切换模块时继承。Windows 同样在原偏好 JSON 中保存 `Transparency`,更新当前可视化的 `Opacity`。缺少偏好时存储值默认为 0,即界面的 100%;存储越界值限制到 0~90。调整不重连采集、不重建动画会话,也不增加计时器。 - -## 分工 - -- `modules/visualizations/api/VisualizationAppearance.swift` 定义平台无关的 RGB 值、五种预设与单色外观。 -- `VisualizationController.applyAppearance(_:)` 更新当前绘制,并在新模块激活前传入当前外观。插件适配把笔色发送为 `appearanceChanged`。 -- `apps/macos/VisualizationAppearanceStore.swift` 保存笔色预设 ID `MiucsVisualizationInk.v1` 及独立的透明度百分比。首次升级时按旧飞鸟小鸟主色选择 RGB 距离最近的预设,旧配色 JSON 原样保留;无有效偏好时使用石墨灰。 -- `VisualizationColorMenu` 提供原生菜单,应用菜单的颜色项也使用同一数据源。选择触发一次外观更新,不增加刷新计时器或逐帧颜色分析。 -- 模块保留各自的线宽、透明度、渐隐与浅色衬线,所有主要笔触使用公共色。五线谱的音名缓存随颜色变化失效。 - -颜色更新不创建或切换会话,不改变窗口,不消费待处理起音,也不清空旋律轨迹。旧配色仅保留读取迁移逻辑;应用使用五色菜单。 - -## 验证 +macOS 的 `VisualizationAppearanceStore` 保存笔色 ID 和透明度百分比,视图应用 `alphaValue`;Windows 保存 `Color` 和 `Transparency`,应用当前视图的不透明度。调整外观不重连采集、不重建会话、不增加动画计时器。缺失或损坏的颜色偏好回退到石墨灰,透明度限制到 0~90。 ```sh zsh scripts/test-appearance.sh -zsh scripts/test-contracts.sh -zsh build.sh +zsh scripts/test-visualization-host.sh +sh scripts/test-plugins.sh ``` - -当前外观测试覆盖偏好迁移、菜单动作、透明度与会话切换;插件像素输出通过 `sh scripts/test-plugins.sh` 验证。历史原生配色报告保留用于追溯,不代表当前插件验收结果。 diff --git a/docs/architecture.md b/docs/architecture.md index 158beed..e93854a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -38,7 +38,7 @@ flowchart LR | `tools/macos/` | 输入诊断与音频文件分析 | | `tests/` | 契约、通用音频、场景规则与各平台集成验证 | -通用 API 不出现原生视图、颜色或 Combine 类型;原生扩展单独归入 `platforms/`。音频核心不引用任何飞鸟或窗口对象。macOS 与 Windows 平台实现互不引用。`scripts/check-boundaries.py` 检查当前 Swift 源码的这些约束,`test-contracts.sh` 不链接 Apple UI、采集或 Accelerate 框架。 +通用 API 不出现原生视图、颜色或 Combine 类型;原生扩展单独归入 `platforms/`。音频核心不引用任何角色或窗口对象。macOS 与 Windows 平台实现互不引用。`scripts/check-boundaries.py` 检查当前 Swift 源码的这些约束,`test-contracts.sh` 不链接 Apple UI、采集或 Accelerate 框架。 这些目录是源码与依赖边界:macOS 由 `swiftc` 分组编译;Windows 由 `apps/windows/Miucs.Windows.csproj` 组合各 Windows 目录的 C# 源码。Lua 插件通过 C ABI 宿主桥接到隔离 worker,不加载第三方原生插件。Windows 的数值、事件、场景和窗口偏好在独立可移植测试中验证,原生平台检查见 `docs/windows-acceptance.md`。 @@ -58,7 +58,7 @@ flowchart LR 通道用短锁保护内存值,不执行回调或 UI 工作,不逐帧序列化。连续状态只有一个最新快照;起音和节拍各保留最多 64 个,超过 250ms 或容量时丢弃最旧项;频谱变化用单调最大值队列保存最近 250ms 的未读区间峰值。订阅者有独立游标,互不排空彼此的事件。 -消费者每次屏幕刷新读一次。帧通过 `sessionID + sequence` 去重;同一帧不会反复重置动作。短事件即使下一帧已恢复平静,也能在后续刷新触发。多起音在飞鸟会话中合为一次振翅;其他模块可自行逐个处理。切换或恢复显示时新建订阅,从当前序号开始,不补播先前动作。 +消费者每次屏幕刷新读一次。帧通过 `sessionID + sequence` 去重;同一帧不会反复重置动作。短事件即使下一帧已恢复平静,也能在后续刷新触发。各插件按自己的动画规则消费起音、节拍和按键事件。切换或恢复显示时新建订阅,从当前序号开始,不补播先前动作。 每次采集重建产生新 UUID,同时清理快照和事件。旧队列的迟到回调因代次不符被拒绝;消费者收到新代次后清除自己的输入历史。音频窗口中心时间用于音高和测量时间线,主机单调接收时间用于事件过期、帧龄和恢复看门狗,二者不混算。 @@ -72,11 +72,11 @@ macOS 的 `PluginVisualizations.registry()` 从插件清单创建描述符;`Pl `VisualizationRegistry`、`VisualizationController` 和平台视图接口是应用内部的窗口适配层。扩展可视化应遵循 [插件契约](../contracts/plugins/README.md),不向应用注册原生类。音频和键盘使用独立的代次、序号和权限;`PluginHost` 同帧打包两路快照与短事件,交给当前插件。 -`plugins/pencil-flight`、`plugins/staff-note`、`plugins/pixel-keyboard` 分别维护飞鸟、音符和像素键盘的状态。新增插件只需提供清单、Lua 和资源。应用猫头图标及 `TrayHeadAnimation` 属于外壳,独立于当前插件;插件切换不影响托盘身份。 +`plugins/pixel-keyboard`、`plugins/muffin-puppies`、`plugins/niulai-keyboard`、`plugins/heiji-kazuha` 分别维护奶牛猫、麦芬、牛来、平次与和叶的状态。奶牛猫为默认插件并在目录中置顶。新增插件只需提供清单、Lua 和资源。应用猫头图标及 `TrayHeadAnimation` 属于外壳,独立于当前插件;插件切换不影响托盘身份。 -全局颜色与透明度由外壳保存。macOS 通过 `VisualizationAppearanceStore` 和 `PluginSession` 传递笔色,Windows 通过 `PluginHost.Appearance` 传递外观。变更外观不重建输入采集或清理动画。旧飞鸟配色仅保留最小读取迁移逻辑,旧调色盘与模型已移除。 +全局颜色与透明度由外壳保存。macOS 通过 `VisualizationAppearanceStore` 和 `PluginSession` 传递笔色,Windows 通过 `PluginHost.Appearance` 传递外观。变更外观不重建输入采集或清理动画。插件保留自己的角色配色,旧专属调色盘与迁移代码已移除。 -窗口负责透明、鼠标穿透、悬停锁、拖动和等比缩放。切换前保存旧位置与尺寸、暂停绘制,释放旧会话,再恢复目标插件视口并激活。隐藏和切换不停止音频采集。每个插件独立保存位置和大小,旧飞鸟窗口偏好键继续兼容,未知选择回退到可用插件。 +窗口负责透明、鼠标穿透、悬停锁、拖动和等比缩放。切换前保存旧位置与尺寸、暂停绘制,释放旧会话,再恢复目标插件视口并激活。隐藏和切换不停止音频采集。每个插件独立保存位置和大小,奶牛猫保持原有 `pixel-keyboard` 标识,未知选择优先回退到奶牛猫。 ## 构建与验证 diff --git a/docs/assets/miucs-demo.gif b/docs/assets/miucs-demo.gif deleted file mode 100644 index 25d41b0..0000000 Binary files a/docs/assets/miucs-demo.gif and /dev/null differ diff --git a/docs/assets/miucs-desktop-demo.gif b/docs/assets/miucs-desktop-demo.gif deleted file mode 100644 index 0b335b3..0000000 Binary files a/docs/assets/miucs-desktop-demo.gif and /dev/null differ diff --git a/docs/assets/miucs-palette.png b/docs/assets/miucs-palette.png deleted file mode 100644 index adacc4b..0000000 Binary files a/docs/assets/miucs-palette.png and /dev/null differ diff --git a/docs/assets/miucs-staff-note.gif b/docs/assets/miucs-staff-note.gif deleted file mode 100644 index 41df709..0000000 Binary files a/docs/assets/miucs-staff-note.gif and /dev/null differ diff --git a/docs/assets/miucs-staff-note.png b/docs/assets/miucs-staff-note.png deleted file mode 100644 index ba9e1e6..0000000 Binary files a/docs/assets/miucs-staff-note.png and /dev/null differ diff --git a/docs/assets/plugins/heiji-kazuha.gif b/docs/assets/plugins/heiji-kazuha.gif new file mode 100644 index 0000000..abd0e38 Binary files /dev/null and b/docs/assets/plugins/heiji-kazuha.gif differ diff --git a/docs/assets/plugins/manifest.json b/docs/assets/plugins/manifest.json index 56c6913..0dc5358 100644 --- a/docs/assets/plugins/manifest.json +++ b/docs/assets/plugins/manifest.json @@ -10,23 +10,10 @@ "ignoredSwitchEvents": [], "plugins": [ { - "id": "muffin-puppies", - "name": "麦芬与线条小狗", + "id": "pixel-keyboard", + "name": "奶牛猫", "version": "1.0.0", - "file": "muffin-puppies.gif", - "width": 640, - "height": 368, - "samples": 192, - "encodedFrames": 168, - "durationMs": 12000, - "inputHash": "75ae93b86000e1e07cc05877ed27560f582611b02122e08ee9f0548b03df6c45", - "inputFrames": 720 - }, - { - "id": "niulai-keyboard", - "name": "牛来", - "version": "0.1.1", - "file": "niulai-keyboard.gif", + "file": "pixel-keyboard.gif", "width": 640, "height": 368, "samples": 192, @@ -36,23 +23,23 @@ "inputFrames": 720 }, { - "id": "pencil-flight", - "name": "铅笔飞鸟", + "id": "muffin-puppies", + "name": "麦芬", "version": "1.0.0", - "file": "pencil-flight.gif", + "file": "muffin-puppies.gif", "width": 640, - "height": 416, + "height": 368, "samples": 192, - "encodedFrames": 192, + "encodedFrames": 168, "durationMs": 12000, "inputHash": "75ae93b86000e1e07cc05877ed27560f582611b02122e08ee9f0548b03df6c45", "inputFrames": 720 }, { - "id": "pixel-keyboard", - "name": "像素音乐键盘", - "version": "1.0.0", - "file": "pixel-keyboard.gif", + "id": "niulai-keyboard", + "name": "牛来", + "version": "0.1.1", + "file": "niulai-keyboard.gif", "width": 640, "height": 368, "samples": 192, @@ -62,14 +49,14 @@ "inputFrames": 720 }, { - "id": "staff-note", - "name": "五线谱音符", + "id": "heiji-kazuha", + "name": "平次与和叶", "version": "1.0.0", - "file": "staff-note.gif", + "file": "heiji-kazuha.gif", "width": 640, - "height": 128, + "height": 368, "samples": 192, - "encodedFrames": 192, + "encodedFrames": 170, "durationMs": 12000, "inputHash": "75ae93b86000e1e07cc05877ed27560f582611b02122e08ee9f0548b03df6c45", "inputFrames": 720 diff --git a/docs/assets/plugins/pencil-flight.gif b/docs/assets/plugins/pencil-flight.gif deleted file mode 100644 index a7536ee..0000000 Binary files a/docs/assets/plugins/pencil-flight.gif and /dev/null differ diff --git a/docs/assets/plugins/staff-note.gif b/docs/assets/plugins/staff-note.gif deleted file mode 100644 index 7f5a928..0000000 Binary files a/docs/assets/plugins/staff-note.gif and /dev/null differ diff --git a/docs/audio-features.md b/docs/audio-features.md index 295f888..529273d 100644 --- a/docs/audio-features.md +++ b/docs/audio-features.md @@ -2,7 +2,7 @@ 更新日期:2026-10-22。本文记录基础音频分析的先行验证阶段。 -后续更新:已实现铅笔音乐飞鸟,以及独立的主旋律近似提取。本文保留基础测量验证记录;最新状态、主旋律准确率与限制见 [飞鸟实现记录](sound-flight.md)。 +本文保留基础测量的历史验证记录。共享分析器的主旋律和节拍能力分别见[主旋律说明](predominant-mixture.md)与[节拍识别](beat-tracking.md)。 ## 目标与边界 @@ -53,7 +53,7 @@ ## 实现约定与验证结果 -结论:本阶段的基础特征在受控信号、独立数值参考与实际系统回放链路上通过验证,可以供 UI 使用。后续新增的主旋律近似已单独评估,准确率与限制见飞鸟实现记录;BPM、人声识别等仍待实现和验证。起音检测目前是规则算法,测试通过不代表在所有音乐上都能准确判断事件。 +结论:本阶段的基础特征在受控信号、独立数值参考与实际系统回放链路上通过验证,可以供 UI 使用。后续新增的主旋律近似已单独评估,准确率与限制见主旋律说明;节拍检测已有独立实现,人声分类不属于当前能力。起音检测目前是规则算法,测试通过不代表在所有音乐上都能准确判断事件。 ### 数据接口与定义 @@ -128,7 +128,7 @@ zsh scripts/analyze-audio-file.sh /absolute/path/input.wav /absolute/path/featur ### 后续 UI 可使用的边界 -可以使用原始能量、分频能量、包络、频谱重心/带宽/平坦度、声道关系和静音来设计连续动效。起音适合做触发效果,并保留误判回退。后续的独立主旋律近似已接入飞鸟,见上述实现记录;BPM/拍相位、语音/乐器类别、情绪等仍未实现。 +可以使用原始能量、分频能量、包络、频谱重心/带宽/平坦度、声道关系和静音来设计连续动效。起音适合做触发效果,并保留误判回退。共享分析器还提供近似主旋律及 BPM/拍相位;语音/乐器类别、情绪等不属于当前能力。 ## 参考资料 diff --git a/docs/beat-tracking.md b/docs/beat-tracking.md index ae1212d..fab5c34 100644 --- a/docs/beat-tracking.md +++ b/docs/beat-tracking.md @@ -1,6 +1,6 @@ -# 节拍识别与拍点圆环 +# 节拍识别与角色动画 -节拍取自系统播放的**原始混音**,与混音主旋律识别并行。每个拍点在画布内随机位置散开涟漪;纯伴奏或歌声暂时没有有效基频时也能触发。音量、谱位、倾摆、旋律轨迹和键盘星爆继续同时工作。 +节拍取自系统播放的原始混音,与共享主旋律分析并行。奶牛猫用节拍驱动尾巴,麦芬吹泡泡,平次与和叶中的和叶随可靠拍点转身;各插件的键盘动作独立运行。 ## 分析与时间 @@ -12,26 +12,15 @@ 拍点时间属于原始 PCM 时间线。通道用帧的主机接收时间减去 `audio.time - beatTime` 得到事件的主机时间,供动画计算已经经过的时间;这不是完整采集/播放延迟测量。每订阅者独立读取,最多保留 64 个、250ms,重复拍点去重。两个新字段都为可选,旧 v1 样本仍能解码。 -## 圆环 +## 动画与生命周期 -每个拍点在随机落点展开两层「轻弧水波」。每层由三段留白弧线组成,主波半径从 4 点扩到 32 点,内层延迟 90ms、最大半径 24 点,各持续 800ms。三道小弧线分别延迟 100、155、210ms 出现,各持续 580ms,半径从 1.5 点扩到 6.2、8.3、5.3 点;圆心由最终距离的 70% 缓慢移动到 28、34、40 点,纵向位移乘 0.8。方向在新拍点时确定,动画期间不随机跳动。 +插件自行处理节拍力度、相位与新拍点。失联、静音、隐藏和重新激活时按各自规则清理或收束动作,不补播隐藏期间的旧事件。奶牛猫在锁定 BPM 前还可响应当前能量与起音;这不等于分析器已经获得可靠拍点。 -整体约 96 点范围,主波最大直径 64 点,纵向压至 0.9 倍;主波/内层/小弧线描边为 1.05/0.72/0.65 点,浅色衬线宽度增量为 0.85 点。35~60ms 轻柔显现,随后缓出扩张、淡出,整组 890ms 内消失。弧线端点使用三档透明度近似预览的渐淡笔触,轻微固定曲率起伏避免机械圆形。落点位于 800 × 160 逻辑画布的 x=112~712、y=48~112;相邻两拍横向至少相隔 120 点。动画独立于音符的音高、位置、缩放和倾摆;应用使用随机种子,合成演示固定种子便于复现。 - -绘制顺序为五线谱 / 轨迹 → 键盘星星 → 拍点轻弧 → 谱位标记 / 音符 / 音名。使用全局五种笔色、浅色衬线和上下各 8 点的渐变遮罩,按窗口比例等比缩放。缓存主波/小波的弧线路径和边缘渐变,预建二十个波纹槽,每槽三组双描边图层,最多保留四次拍点;复用屏幕刷新,超过 150ms 的新事件丢弃,长帧直接淘汰旧波纹,不追赶补播。 - -隐藏、切换模块、音频失联、静音、格式/代次改变会清空圆环;音高变化或人声模型缺失不会清空。键盘星爆仍按自己的生命周期工作。 - -## 验证与边界 - -`scripts/test-beat-tracker.sh` 将合成鼓点 PCM 送入生产基础分析器,覆盖 44.1/48kHz、50/60/90/120/150/180/200 BPM、弱细分、鼓点间静音、随机起音、稳定长音、变速、停顿和非法输入。稳定后拍点相位误差 p95 约 11~25ms;首拍在开始后约 2.7~5.2 秒出现,慢速节奏需要更长的历史。这些是合成输入的检测时间,不是完整音画延迟或真实歌曲准确率。详见 [检测报告](validation/beat-tracker-tests.json)。 - -真实音乐存在半速/倍速歧义;弱鼓点、自由速度、复杂切分及不足几个周期的短片段可能无法稳定锁定,此时圆环会减少或暂停。现有《童话》短片段只有音高标注,没有逐拍标注,检查它们仅用于确认生产链路能够消费真实混音,不能作为节拍准确率评测。逐片重置的 31 段测试里,8 段触发了 51 个拍点,其余段落未锁定;保留[完整触发记录](validation/beat-song-smoke.json)。这说明本版对短、弱节奏片段仍较保守。 - -[圆环集成测试](validation/staff-beat-tests.json) 验证随机落点覆盖范围、相邻落点间距、圆心不跟随音符、无歌声音高时触发、全部动效并行、谱位不变、生命周期、图层复用、五色、600/800/1600 点及浅深背景。[轻弧版本并行压测](validation/staff-beat-arcs-performance.json)覆盖每秒 20 次按键与 112 颗星星/20 个轻弧波纹满载、600/800/1600 点;主线程 CPU 更新及提交以 p95 小于 2ms 为目标,实际数据见报告。压测同时包含连续音高缺口的补线;CPU 耗时不包含调度等待及 WindowServer/GPU 执行,完整墙钟耗时保留在报告中。另行记录实际透明窗口刷新间隔。此前紧凑圆环版本的[800 点透明窗口 15 秒实测](validation/staff-beat-cadence-800.json)在当时桌面负载下平均约 46.98fps,超过 25ms 的刷新间隔有 116 次;未模拟音高缺口的同机对照平均约 51.60fps。两次测量都存在长帧,不能只凭 CPU 耗时推断稳定刷新率或将卡顿归因于某一种效果。隐藏停止绘制,恢复后一秒正常刷新。 +## 验证 ```sh zsh scripts/test-beat-tracker.sh +sh scripts/test-plugins.sh ``` -轻弧版本验证见[汇总](validation/staff-beat-arcs-validation.json):29 项集成检查通过;主线程 CPU 更新与提交 p95 为 0.078~0.092ms。另测 [800 点透明窗口 6 秒刷新](validation/staff-beat-arcs-cadence-800.json),平均约 60.00fps,有 2 次间隔超过 25ms,最长 26.34ms;同时运行每秒 20 次合成按键、节拍和音高缺口平滑补线。该短测不代表所有桌面负载均无掉帧。 +共享算法测试使用合成鼓点 PCM,覆盖采样率、速度变化、弱细分、静音和非法输入。真实音乐可能存在半速/倍速歧义,短片段、自由节奏或弱鼓点未必能锁定。合成输入通过不能替代真实歌曲或完整音画延迟测量;当前皮肤规则见各插件 README。 diff --git a/docs/concepts/heiji-kazuha/heiji-prompt.txt b/docs/concepts/heiji-kazuha/heiji-prompt.txt new file mode 100644 index 0000000..5673df0 --- /dev/null +++ b/docs/concepts/heiji-kazuha/heiji-prompt.txt @@ -0,0 +1,3 @@ +Create a production TRANSPARENT sprite atlas derived from the LEFT character in the supplied approved skin concept. Only Hattori Heiji, no keyboard or other character. Preserve his recognizable tan skin, black hair, backwards white-and-blue baseball cap, dark navy kendo jacket/hakama, two hands gripping a warm brown WOODEN bokken. Crisp 16-bit pixel art with stepped edges, small chibi proportions matching reference. Actual RGBA transparency, no colored/checkerboard background, NO shadows, NO text, NO cell borders, NO numbers. +EXACTLY 12 complete full-body animation frames in a perfectly regular 4-column by 3-row grid. Canvas landscape 1536x1024. Each cell 384x341 approximately. All 12 sprites SAME scale, same identity and outfit, same orientation (three-quarter toward viewer's left); feet anchored at same local x=192,y=296 in EVERY cell. Character head-to-toe height about 210 pixels. Whole sword must fit inside its cell with 30px blank padding on all sides. No part of character or sword crosses cells. Do not enlarge characters to fill cells. Leave transparent separation. Consistent character size throughout and grounded stable stance, no camera changes. +Reading left-to-right then top-to-bottom, show ONE continuous believable two-handed kendo wooden-sword swing and recovery: 1 resting guard, sword diagonal forward-left; 2 elbows bend and start raising sword; 3 sword above forehead; 4 fully raised overhead, knees bend; 5 begin forceful downswing; 6 mid diagonal slash to left; 7 follow-through sword pointing low-left; 8 deepest low-left follow-through and forward knee bend; 9 begin lifting back from low-left; 10 body settles upright and sword returns; 11 almost resting guard; 12 original resting guard. Keep hands on handle, preserve wooden sword length, do not use a metal sword, no duplicate swords. Frames 5–8 may have very small restrained blue-gray pixel motion accent next to sword, never a huge arc. This is twelve separately drawn animation poses, not twelve duplicate figures. Copy reference character appearance closely but OUTPUT ONLY the transparent animation atlas. diff --git a/docs/concepts/heiji-kazuha/kazuha-prompt.txt b/docs/concepts/heiji-kazuha/kazuha-prompt.txt new file mode 100644 index 0000000..278ee69 --- /dev/null +++ b/docs/concepts/heiji-kazuha/kazuha-prompt.txt @@ -0,0 +1,12 @@ +Production sprite atlas, actual transparent RGBA. Draw Toyama Kazuha from the RIGHT of the reference skin, matching her chibi pixel art identity, brown high ponytail, green ribbon, peach-pink floral kimono, green obi bow at back, white tabi and sandals. ONLY Kazuha sprites; no keyboard, text, arrows, grid lines, background or shadows. +Exactly EIGHT frames arranged in FOUR equal columns and TWO equal rows on a landscape canvas. One full-body same-scale character per cell, generous transparent gutters, feet aligned at same local baseline, all sprites same physical height. Crisp square-pixel 16-bit art, no smooth painting. The eight frames are the eight consecutive compass views of ONE clockwise turn in place. The camera NEVER changes. Maintain clothing and hairstyle across all eight sides. +Read order left-to-right top-to-bottom: +TOP ROW cell1 FRONT: full front face, two eyes, obi belt front. +TOP ROW cell2 FRONT-LEFT three-quarter: face and nose point screen LEFT, both eyes may be visible, torso turned left 45 degrees. +TOP ROW cell3 LEFT PROFILE: nose points screen LEFT, exactly one eye, body entirely side-on, green obi bow extends behind to screen RIGHT. +TOP ROW cell4 REAR-LEFT three-quarter: back of head and big obi bow dominate, only tiny left facial edge at screen LEFT, no visible front chest. +BOTTOM ROW cell1 BACK: back of head, ponytail, centered green obi bow, absolutely NO face. +BOTTOM ROW cell2 REAR-RIGHT three-quarter: back and obi bow dominate, tiny face edge at screen RIGHT, no front chest. +BOTTOM ROW cell3 RIGHT PROFILE: nose points screen RIGHT, exactly one eye, body entirely side-on, obi bow behind to screen LEFT. +BOTTOM ROW cell4 FRONT-RIGHT three-quarter: face and nose point screen RIGHT, front chest and obi belt visible, turns back toward FRONT to close loop. +These specific orientations are essential: first row turns toward screen LEFT, last two frames face screen RIGHT. Do not repeat left-facing profile at the end. Relaxed happy dance pose, arms a little spread, modest full-length kimono follows slight spin, ponytail trails naturally. Do not add an extra ninth pose. Character bodies stay upright. Same character scale and baseline throughout. This is an eight-direction 360-degree spin animation sprite sheet, not arbitrary cute poses. diff --git a/docs/concepts/heiji-kazuha/registration.json b/docs/concepts/heiji-kazuha/registration.json new file mode 100644 index 0000000..ccc0abd --- /dev/null +++ b/docs/concepts/heiji-kazuha/registration.json @@ -0,0 +1,254 @@ +{ + "heiji": { + "source": "assets/heiji-swing.png", + "sha256": "1224c03a586cb3f7047f8c9d52328e6c94d7c8460995de3f0489c223106ae42e", + "frames": [ + { + "rect": [ + 52, + 104, + 268, + 253 + ], + "anchor": [ + 175.0, + 251 + ] + }, + { + "rect": [ + 462, + 89, + 246, + 268 + ], + "anchor": [ + 151.0, + 266 + ] + }, + { + "rect": [ + 880, + 25, + 197, + 332 + ], + "anchor": [ + 100.5, + 330 + ] + }, + { + "rect": [ + 1253, + 30, + 219, + 327 + ], + "anchor": [ + 105.5, + 325 + ] + }, + { + "rect": [ + 57, + 419, + 273, + 254 + ], + "anchor": [ + 176.0, + 252 + ] + }, + { + "rect": [ + 438, + 419, + 296, + 256 + ], + "anchor": [ + 197.0, + 254 + ] + }, + { + "rect": [ + 813, + 426, + 313, + 248 + ], + "anchor": [ + 211.5, + 246 + ] + }, + { + "rect": [ + 1167, + 458, + 321, + 216 + ], + "anchor": [ + 224.0, + 214 + ] + }, + { + "rect": [ + 63, + 731, + 276, + 252 + ], + "anchor": [ + 170.0, + 250 + ] + }, + { + "rect": [ + 462, + 722, + 255, + 261 + ], + "anchor": [ + 155.5, + 259 + ] + }, + { + "rect": [ + 813, + 729, + 270, + 254 + ], + "anchor": [ + 170.5, + 252 + ] + }, + { + "rect": [ + 1190, + 729, + 276, + 253 + ], + "anchor": [ + 178.0, + 251 + ] + } + ] + }, + "kazuha": { + "source": "assets/kazuha-turn.png", + "sha256": "0ead7f8e955deb8b4d20a2592eaa673a863d805f17d2f32cd30eeb518971a4d2", + "frames": [ + { + "rect": [ + 52, + 52, + 336, + 414 + ], + "anchor": [ + 148.5, + 412 + ] + }, + { + "rect": [ + 441, + 57, + 315, + 409 + ], + "anchor": [ + 118.0, + 407 + ] + }, + { + "rect": [ + 820, + 59, + 320, + 407 + ], + "anchor": [ + 109.0, + 405 + ] + }, + { + "rect": [ + 1179, + 60, + 345, + 406 + ], + "anchor": [ + 139.5, + 404 + ] + }, + { + "rect": [ + 51, + 548, + 330, + 400 + ], + "anchor": [ + 184.0, + 398 + ] + }, + { + "rect": [ + 407, + 545, + 337, + 403 + ], + "anchor": [ + 198.0, + 401 + ] + }, + { + "rect": [ + 779, + 541, + 317, + 407 + ], + "anchor": [ + 191.5, + 405 + ] + }, + { + "rect": [ + 1154, + 537, + 338, + 413 + ], + "anchor": [ + 199.0, + 411 + ] + } + ] + } +} diff --git a/docs/concepts/heiji-kazuha/skin-concept.png b/docs/concepts/heiji-kazuha/skin-concept.png new file mode 100644 index 0000000..13bc94d Binary files /dev/null and b/docs/concepts/heiji-kazuha/skin-concept.png differ diff --git a/docs/concepts/pixel-cat-tail/README.md b/docs/concepts/pixel-cat-tail/README.md index 1ca9db5..d0df254 100644 --- a/docs/concepts/pixel-cat-tail/README.md +++ b/docs/concepts/pixel-cat-tail/README.md @@ -1,6 +1,6 @@ # 像素猫尾巴:概念与动作参考 -确认的 v2 概念已在 macOS 与 Windows 的 `PixelTailScene` 中重建为缓存的像素矢量动画,保持固定尾根和稳定粗细,并由实际拍点驱动。生产实现与灵敏度参数见[像素音乐键盘说明](../../pixel-keyboard.md),实际原生合成演示见[主页 GIF](../../assets/miucs-pixel-keyboard.gif)。以下保留设计过程及生成提示词。 +确认的 v2 概念已转为奶牛猫插件的尾巴图集和 Lua 节拍动画,保持固定尾根和稳定粗细,并由实际拍点驱动。生产实现与灵敏度参数见[奶牛猫说明](../../pixel-keyboard.md),实际原生合成演示见[主页 GIF](../../assets/miucs-pixel-keyboard.gif)。以下保留设计过程及生成提示词。 ## v2:收细与循环预览 diff --git a/docs/hover-lock.md b/docs/hover-lock.md index 7ed0651..5ea9315 100644 --- a/docs/hover-lock.md +++ b/docs/hover-lock.md @@ -18,7 +18,7 @@ 1. 在下层普通应用上方停留不足 3 秒,再移出/返回;只在新的完整等待后出现锁。 2. 锁定时点击画面不同位置,下层应用正常响应;仅小锁按钮截获点击,不改变前台应用焦点。 3. 连续开锁/上锁 20 次,拖动、右下角缩放,菜单状态始终同步,无残留鼠标捕获。 -4. 切换飞鸟/五线谱,隐藏/恢复、睡眠/唤醒,按钮无残留,位置尺寸仍分别保存。 +4. 切换四个插件,隐藏/恢复、睡眠/唤醒,按钮无残留,位置尺寸仍分别保存。 5. 混合 DPI、多屏负坐标、跨屏拖动、拔除副屏、修改分辨率,锁始终位于画面中心偏上。 验收只使用合成输入;不上传个人桌面、真实音频或键盘内容。 diff --git a/docs/melody-benchmark.md b/docs/melody-benchmark.md index 5178231..85a7ac4 100644 --- a/docs/melody-benchmark.md +++ b/docs/melody-benchmark.md @@ -1,6 +1,6 @@ # 《童话》主旋律识别对照 -2026-10-24。用有独立参考的真实音乐检验共享 `MelodyAnalyzer`,分别记录实际音高误差、谱面音符一致性和伴奏误报。五线谱固定音高对应固定谱位的规则保持不变。 +2026-10-24。用有独立参考的真实音乐检验共享 `MelodyAnalyzer`,分别记录实际音高误差、谱面音符一致性和伴奏误报。此记录针对共享分析器,历史谱面指标不表示当前插件行为。 ## 音源与参考 diff --git a/docs/pixel-keyboard.md b/docs/pixel-keyboard.md index bc60643..eb1833a 100644 --- a/docs/pixel-keyboard.md +++ b/docs/pixel-keyboard.md @@ -1,14 +1,14 @@ -# 像素音乐键盘 +# 奶牛猫 -![像素音乐键盘:工作台合成音乐与打字输入预览](assets/plugins/pixel-keyboard.gif) +![奶牛猫:工作台合成音乐与打字输入预览](assets/plugins/pixel-keyboard.gif) 工作台使用统一合成输入生成的 12 秒循环 GIF,无声。查看[全部插件预览](plugin-previews.md)。 -第三个内置模式:横向留白显示屏、逐键绘制的像素键盘、左右黑猫白手套猫爪。屏幕最左边放置根据用户照片转制的透明像素猫头,保留黄绿色眼睛和白下巴;头像占 64×64 点;右侧纯黑像素尾巴横向伸展约 80 点长,活动区域为 x=652~748、y=40~128,中间 x=116~638 留白。画布 800×460,宽度 80~1600,保持比例并服从屏幕可用范围。使用现有悬停锁、拖动、缩放及各模块独立位置保存。 +Miucs 的默认主打插件(稳定 ID:`pixel-keyboard`):横向留白显示屏、逐键绘制的像素键盘、左右黑猫白手套猫爪。屏幕最左边放置根据用户照片转制的透明像素猫头,保留黄绿色眼睛和白下巴;头像占 64×64 点;右侧纯黑像素尾巴横向伸展约 80 点长,活动区域为 x=652~748、y=40~128,中间 x=116~638 留白。画布 800×460,宽度 80~1600,保持比例并服从屏幕可用范围。使用现有悬停锁、拖动、缩放及各模块独立位置保存。 ## 音乐与按键 -- 屏幕不再绘制五线谱、旋律轨迹、音名、星爆或涟漪。独立五线谱模式保持原样。尾巴消费现有拍点事件,连续扭动的弯曲从根部传向尾尖,尾根固定,尾尖圆钝、轻轻上翘并滞后跟随,尾身约 12~15 点粗并带少量像素毛边;BPM 平滑控制运动速度(约两拍一个完整周期),拍点强度影响起伏,弱拍也保留明显摆幅;起摆响应时间常数为 55ms,尾尖最大起伏约 ±13 点,不在新拍点强行翻向或重置相位。 +- 尾巴消费现有拍点事件,连续扭动的弯曲从根部传向尾尖,尾根固定,尾尖圆钝、轻轻上翘并滞后跟随,尾身约 12~15 点粗并带少量像素毛边;BPM 平滑控制运动速度(约两拍一个完整周期),拍点强度影响起伏,弱拍也保留明显摆幅;起摆响应时间常数为 55ms,尾尖最大起伏约 ±13 点,不在新拍点强行翻向或重置相位。 - 节拍模式只接收 150ms 内、置信度至少 0.45 的 50~200 BPM 拍点。BPM 尚未锁定或拍点超时后,使用 150ms 内、强度至少 0.18 的有效起音触发短促扭动,保持约 420ms 后衰减;起音间隔至少 120ms,不将起音解释成稳定 BPM。可靠拍点优先,同帧不重复触发,也不积压旧动作。静音、音频失联或事件超时后约 300ms 回到静止姿态;隐藏立即复位,恢复不补播历史事件。 - 猫头沿用原图,绕下巴附近的固定支点左右轻摇,最大 ±12°。频率为最近一秒普通按键次数 × 0.4Hz,最多 2.4Hz;速度平滑过渡,快慢只影响频率。系统重复和修饰键不计入打字速度,必须收到新的有效按下才开始摇头。停输后平缓回正;安全输入、权限丢失、丢失事件、隐藏与代次变化复位,恢复不补播旧动作。音频重连不影响摇头。 - 键盘覆盖 ANSI 主键区五行,按物理键码映射,不依赖输入法。键帽显示固定布局名,macOS 底行用 Fn/Ctrl/Opt/Cmd,Windows 用 Ctrl/Win/Alt。功能键、方向键、导航区、小键盘首版不显示。 diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 8424807..30c5b61 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -258,14 +258,15 @@ end Lua 不能直接访问网络、文件系统、原始 PCM 或输入文字。独立 worker 用于故障隔离,并非操作系统级安全沙箱;安装插件仍需信任其来源。运行时限制、资源上限和性能要求见[插件 API](../contracts/plugins/README.md)。 -可直接参考三个相互独立的内置包: +可直接参考四个独立的内置包: -- [铅笔飞鸟](../plugins/pencil-flight/):音频特征映射、路径与粒子。 -- [五线谱音符](../plugins/staff-note/):旋律轨迹、键盘星星与节拍轻弧。 -- [像素音乐键盘](../plugins/pixel-keyboard/):物理按键、图集和像素动画。 +- [奶牛猫](../plugins/pixel-keyboard/):默认主打,物理按键、图集和像素动画。 +- [麦芬](../plugins/muffin-puppies/):跳跃帧、手绘键盘与节拍泡泡。 +- [牛来](../plugins/niulai-keyboard/):按键和音乐的独立动作。 +- [平次与和叶](../plugins/heiji-kazuha/):挥剑与节拍转身。 修改宿主或运行时的开发者可继续阅读[贡献指南](../CONTRIBUTING.md)和[插件迁移与验收记录](plugin-migration.md)。 ## 虚拟运行与测试环境 -运行 `python3 tools/plugins/lab.py serve --plugin plugins/staff-note` 打开本地插件实验台,可模拟音频、键盘、重连、权限和窗口生命周期,固定种子回放并导出截图与诊断。完整用法、场景格式及覆盖边界见 [插件实验台](plugin-lab.md)。 +运行 `python3 tools/plugins/lab.py serve --plugin plugins/pixel-keyboard` 打开本地插件实验台,可模拟音频、键盘、重连、权限和窗口生命周期,固定种子回放并导出截图与诊断。完整用法、场景格式及覆盖边界见 [插件实验台](plugin-lab.md)。 diff --git a/docs/plugin-lab.md b/docs/plugin-lab.md index b42a920..8328c0e 100644 --- a/docs/plugin-lab.md +++ b/docs/plugin-lab.md @@ -9,13 +9,13 @@ ```sh # macOS sh scripts/build-plugins.sh -python3 tools/plugins/lab.py serve --plugin plugins/staff-note +python3 tools/plugins/lab.py serve --plugin plugins/pixel-keyboard ``` ```powershell # Windows,使用项目原有 MSVC / CMake 构建环境 ./scripts/build-windows.ps1 -python tools/plugins/lab.py serve --plugin plugins/staff-note +python tools/plugins/lab.py serve --plugin plugins/pixel-keyboard ``` 命令会打开本地浏览器。也可加 `--no-browser --port 8765`,手动打开终端打印的 `http://127.0.0.1:8765/`。服务只绑定 IPv4 回环地址,API 校验 Host、Origin 和每次启动生成的会话 token;不提供远程访问,不上传数据。 @@ -90,7 +90,7 @@ python tools/plugins/lab.py serve --plugin plugins/staff-note ```sh python3 tools/plugins/lab.py run --plugin plugins/pixel-keyboard --scenario keyboard --output build/lab-keyboard -python3 tools/plugins/lab.py run --plugin plugins/staff-note --scenario /path/to/scenario.json --output build/lab-custom +python3 tools/plugins/lab.py run --plugin plugins/pixel-keyboard --scenario /path/to/scenario.json --output build/lab-custom python3 tests/plugins/lab_test.py ``` diff --git a/docs/plugin-migration.md b/docs/plugin-migration.md index 8a9a991..4ac922a 100644 --- a/docs/plugin-migration.md +++ b/docs/plugin-migration.md @@ -1,101 +1,29 @@ -# UI plugin migration +# 插件运行时与验证 -To create your own plugin, start with the [插件开发指南](plugin-development.md). This document covers runtime migration, implementation details and acceptance status. +Miucs 只使用 Lua 5.4/Skia 可视化。内置四个独立插件: -The shared Lua + Skia implementation is the only visualization runtime. Native visualization implementations and mode switches have been removed. Physical-machine, latency, performance and sustained-memory acceptance gates remain separate from this product decision. +| 菜单名称 | 稳定 ID | 默认顺序 | +| --- | --- | --- | +| 奶牛猫 | `pixel-keyboard` | 1,首次启动默认 | +| 麦芬 | `muffin-puppies` | 2 | +| 牛来 | `niulai-keyboard` | 3 | +| 平次与和叶 | `heiji-kazuha` | 4 | -## Build and run +重命名保留插件 ID,因此奶牛猫已有的选择、位置和尺寸继续有效。失效的保存选择回退到奶牛猫;有效的其他选择继续保留。用户安装的自定义插件排在内置插件之后。 -Requirements: Python 3.10 with the current security updates (or 3.12), CMake 3.24+, platform C++ toolchain, Xcode/Swift on macOS, Visual Studio C++ build tools and .NET 8 on Windows. Dependencies are downloaded over HTTPS and SHA-256 verified by `scripts/prepare-plugin-deps.py`. First build downloads Lua, nlohmann JSON, matching Skia headers and prebuilt static Skia. Currently supported native dependency targets: Mac arm64, Windows x64 and Windows arm64. +每个插件在隔离 worker 中运行 Lua;宿主将音频和键盘快照打包传输,Skia 在宿主绘制。macOS 使用 Metal,Windows 使用 D3D12/DirectComposition,初始化失败时回退到 CPU。应用猫图标、窗口、输入采集与恢复独立于插件。 -macOS: - -```sh -zsh build.sh -dist/Miucs.app/Contents/MacOS/Miucs -``` - -Windows, from PowerShell with the C++ build tools available: - -```powershell -./scripts/build-windows.ps1 -Runtime win-x64 -./dist/windows/win-x64/Miucs.exe -``` - -All builds include the plugin runtime and bundled folders. A missing runtime is an error; there is no native visualization fallback or reduced build. Use `MIUCS_PLUGIN_DEV=1` for file-change reloads and `MIUCS_FORCE_CPU=1` to exercise the Skia CPU backend. CPU presentation is capped at 30 FPS. - -The three built-in IDs (`pencil-flight`, `staff-note`, `pixel-keyboard`) and their window preference keys are unchanged. Each is now an independent folder under `plugins/`, with its own manifest, source and assets. The **可视化** menu lists these plugins directly, including installed user plugins. The application owns the cat logo and tray animation; they do not load a Lua worker and keep working when a plugin fails or is reloaded. Native capture and analysis remain platform implementations; the plugin modules have no capture controls. - -## Package layout - -```text -plugins/ -├── pixel-keyboard/ # pixel music keyboard -├── pencil-flight/ # pencil bird -└── staff-note/ # staff, notes and ripples -``` - -Each folder can be copied, updated and validated independently. No sibling folder is needed. `apps/assets` holds the application cat portrait and packaged icons; regenerate the `.icns` / `.ico` files on macOS with `python3 scripts/generate-app-icons.py`. The pixel keyboard has its own copy of the portrait for its UI. - -## Author a package - -```sh -cp -R tools/plugins/template /path/to/my-plugin -# Edit the manifest ID, main.lua, scene.lua and local assets. -python3 tools/plugins/validate.py /path/to/my-plugin -python3 tools/plugins/preview.py --plugin /path/to/my-plugin --output build/my-preview -``` - -The validator starts the real isolated worker, checks its single UI with synthetic input, decodes resources, and runs lifecycle callbacks. The preview exports PNG files through the same Skia CPU renderer. The manifest declares `id`, `name` and `layout` at the top level; the scene root uses the same `id`. Legacy `visualizations` arrays and `tray` entries are rejected. Both tools use only Python's standard library. See `contracts/plugins/README.md`, `manifest.schema.json` and `api.lua` for the protocol and editor definitions. - -Install by copying the complete folder into the platform user plugin directory and choosing the rescan menu action. Active instances survive a failed rescan/reload. Removing a folder removes its entry on rescan; the current already-loaded instance may remain until switched or reloaded. A changed manifest ID is a new package, not an update to the prior identity. - -## Source map - -| Area | Location | -|---|---| -| Pure scene model, resource validation, Lua runtime | `modules/plugins/src/model.*`, `resources.cpp`, `runtime.*` | -| Worker, bounded binary transport, watchdog | `worker.cpp`, `client.*` | -| Shared Skia painter and C ABI | `renderer.*`, `bridge.cpp`, `include/miucs_plugins.h` | -| Metal / D3D12 + DirectComposition | `surface_mac.mm`, `surface_win.cpp` | -| Platform feature adaptation, package lifecycle, reload | `modules/plugins/platforms/macos`, `modules/plugins/platforms/windows` | -| Physical key names | `modules/keyboard/platforms/*/*PhysicalKeys*` | -| Shared built-in animations and drawings | `plugins/pixel-keyboard`, `plugins/pencil-flight`, `plugins/staff-note` | -| Template, synthetic fixture, validation and previews | `tools/plugins` | -| Worker, transport, replay, native surface and reload tests | `tests/plugins`, `scripts/test-plugin*` | - -No third-party native extension ABI, marketplace, automatic download/update, arbitrary shaders or full application-page UI is included. - -## Verification commands +Lua 内存、场景、图像、控制队列和脚本执行时间均有限额。输入更新合并到最新快照,短事件保留有界队列;无变化补丁跳过场景复制。切换释放旧 worker 和 surface;热重载先验证候选,再替换当前 worker。Metal drawable 使用作用域释放,CPU surface 销毁时移除子图层并清空最后一帧图像。 ```sh sh scripts/build-plugins.sh sh scripts/test-plugins.sh zsh scripts/test-plugin-macos.sh python3 tools/plugins/fixture.py -build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/staff-note build/plugin-fixture.json -MIUCS_FORCE_CPU=1 build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/staff-note build/plugin-fixture.json +build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/pixel-keyboard build/plugin-fixture.json +MIUCS_FORCE_CPU=1 build/plugins/plugin-surface-probe build/plugins/miucs-plugin-worker plugins/pixel-keyboard build/plugin-fixture.json build/plugins/plugin-benchmark plugins build/plugin-fixture.json python3 tests/plugins/soak.py --seconds 1800 ``` -On Windows pass the parent `plugins` directory to the surface probe; it starts a separate worker for each UI. The native probe and worker live under `build/plugins/Release` with `.exe` suffixes. `transport_test.py`, validator and preview select that build layout automatically. `.github/workflows/plugins.yml` builds both native paths and saves preview artifacts; adding a workflow does not mean it has run on the current unpushed branch. - -The replay compiles frozen pre-removal simulations and checks 600 frames per output against Lua, including head/tail, flight position/scale/angle/score, staff motion, trace commands and ring geometry. Fixtures cover short pitch dropouts, silence, long frame intervals, separate input sessions and audio format changes. Host/worker tests cover bounded scene memory, node count, empty path patches, deterministic seeds, revision resync, bytecode, path escapes, corrupt resources, invalid binary packets, event coalescing and a loop that catches its Lua interrupt. Mac reload tests verify candidate failure preservation and viewport continuity. - -`plugin-benchmark` measures script wall time and binary decode + atomic scene application separately after warmup. `plugin-surface-probe` measures CPU time spent in native rendering/submission, **not GPU completion or input-to-photon latency**. The soak uses CPU raster export, switching and worker restarts; its pacing overruns are not a GPU dropped-frame result. Run performance acceptance on otherwise idle, fixed machines with the same Release build, size and DPI as the native baseline. - -## Release gates - -Track measured local results in `docs/validation/plugin-runtime.md`. Plugins are the only runtime. Historical measurements and the frozen numerical fixture remain available for comparison; the removed native source can be recovered from Git history. The following gates still require validation: - -- Native Windows D3D12/DirectComposition transparency, resize/DPI, mouse passthrough, layered WPF interaction, suspend/resume and CPU fallback. -- Native macOS transparency, backing-scale changes, mouse passthrough, hidden/resume, GPU loss and CPU fallback. -- Shared fixture state/path agreement and reviewed screenshots on both platforms, including rings' full area and staff reconnection. -- Isolated fixed-machine Release P95 script update and scene application each ≤ 2 ms; GPU completion separately measured, sustained drops < 1%, added input-to-display P95 ≤ one frame. -- Thirty-minute sustained tests on each target, with repeated switching and reload, showing no sustained memory growth after cache warmup. -- No plugin animation timer when the floating output is hidden; the application tray has its own 16 FPS cadence. Errors leave capture, the cat logo and menus available. - -The runtime's 50 ms CPU failure ceiling and watchdog are fault isolation limits, not relaxed performance acceptance criteria. Font rasterization and GPU antialiasing may differ slightly across platforms; no pixel-identical promise is made. - -Windows presentation notes: the native popup is unowned and kept immediately below the WPF window, because Win32 owned popups are constrained above their owner ([window relationships](https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows)). The surface handles `WM_NCHITTEST` with `HTTRANSPARENT`; actual cross-process mouse passthrough remains a native acceptance check ([hit-test semantics](https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest)). +macOS 与 Windows 共享插件场景和输入契约。离屏绘制、合成回放和 CI 不能替代真实采集、权限、混合 DPI、GPU 和长期内存验证;Windows 状态见[实机验收表](windows-acceptance.md)。 diff --git a/docs/plugin-previews.md b/docs/plugin-previews.md index c8cf1f8..be9a35f 100644 --- a/docs/plugin-previews.md +++ b/docs/plugin-previews.md @@ -1,13 +1,12 @@ -# 插件动态预览 +# 四个桌面伙伴的动态预览 -以下 GIF 由插件工作台使用同一组「音乐与打字」合成输入导出,每张循环 12 秒,采样 16 FPS,最长边 640 像素。正式 Lua worker 与 Skia CPU 绘制器生成画面;GIF 无声,使用暖白背景合成,不是桌面录屏。 +GIF 使用同一组「音乐与打字」合成输入,正式 Lua worker 与 Skia CPU 绘制器生成;每张循环 12 秒、采样 16 FPS、最长边 640 像素。暖白背景仅用于预览,应用浮窗是透明的。GIF 无声,不是桌面录屏。 | 插件 | 说明与动态预览 | | --- | --- | -| 像素音乐键盘 | [查看](pixel-keyboard.md) | -| 五线谱音符 | [查看](staff-note.md) | -| 铅笔飞鸟 | [查看](sound-flight.md) | +| **奶牛猫 · 默认主打** | [查看](pixel-keyboard.md) | +| 麦芬 | [查看](../plugins/muffin-puppies/README.md) | | 牛来 | [查看](../plugins/niulai-keyboard/README.md) | -| 麦芬与线条小狗 | [查看](../plugins/muffin-puppies/README.md) | +| 平次与和叶 | [查看](../plugins/heiji-kazuha/README.md) | -导出于 2026-10-30,随机种子为 42。五个插件的公共输入 SHA-256 一致;尺寸、帧数和时长见[导出清单](assets/plugins/manifest.json)。重新生成的方法见[插件工作台](plugin-lab.md#所有皮肤的统一输入-gif)。 +导出于 2026-10-30,随机种子为 42。四个插件使用相同公共输入;尺寸、帧数和时长见[导出清单](assets/plugins/manifest.json)。重新生成的方法见[插件工作台](plugin-lab.md#所有皮肤的统一输入-gif)。 diff --git a/docs/predominant-mixture.md b/docs/predominant-mixture.md index f0f6a76..b2b7609 100644 --- a/docs/predominant-mixture.md +++ b/docs/predominant-mixture.md @@ -6,7 +6,6 @@ 本次启用已有、经过混音基准验证的 `MelodyAnalyzer`,没有更换它的阈值或按本轮标注调参。它在 4096 点窗口 / 1024 点步长内插值频谱峰,进行有界局部白化、基频与泛音投票、次谐波惩罚,以及前后候选连续性比较。原始 RMS 需大于 0.0005,选中峰能量集中度需大于 0.42,最终可靠度至少 0.48;不可靠时输出空音高。无可靠候选超过 350ms 后清除旧音高偏好。 -五线谱继续只接收 G2~C6;每个音对应固定高度。半音/全音可直接跟随,较大跳跃保留原混音路径的 100/125/150ms 连续确认和半音边界滞回。显示范围限制仅用于谱面,不用来分类人声。静音不报音、不延长旧音符;真实候选恢复后沿用已有的平滑补线。 运行路径已统一为混音主旋律。人声来源菜单、偏好、SDK/模型、分离延迟补偿、模型专用重采样和推理工具已移除;原有混音可靠性判断及轨迹平滑连接保留。旧来源偏好不会影响当前启动。 @@ -24,7 +23,7 @@ | 每片段少于 200ms 的缺失段数 | 12.23 | 0.74 | | 每片段相邻候选跳跃超过 7 半音次数 | 6.26 | 13.13 | -31 段中有 24 段歌声命中率提高,7 段下降。输出更连续,但新增输出不全是正确主唱音高,乐器争抢和八度偏差仍会发生。上面的跳跃/缺口是**分析器输出**,还未经过五线谱大跳确认,不代表屏幕上同样数量的跳跃。 +31 段中有 24 段歌声命中率提高,7 段下降。输出更连续,但新增输出不全是正确主唱音高,乐器争抢和八度偏差仍会发生。上面的跳跃/缺口是**分析器输出**,不代表插件画面上同样数量的动作。 歌手休止时,新模式有 98.92% 的参考帧仍输出乐器候选,旧模式为 40.40%。因为标注只有歌声,此项只表示“歌手休止期间仍有输出”,不能直接叫新模式的误报率,也不能证明乐器音高正确。若仍用“无歌声必须不报音”的旧任务评分,整体正确率反而为 68.79% → 59.45%;本次任务已允许主奏乐器,不能混用两个目标宣称全面提升。 diff --git a/docs/sound-flight.md b/docs/sound-flight.md deleted file mode 100644 index 6b2013a..0000000 --- a/docs/sound-flight.md +++ /dev/null @@ -1,145 +0,0 @@ -# 铅笔音乐飞鸟:实现与验收 - -![铅笔飞鸟:工作台合成音乐与打字输入预览](assets/plugins/pencil-flight.gif) - -工作台使用统一合成输入生成的 12 秒循环 GIF,无声。查看[全部插件预览](plugin-previews.md)。 - -当前可视化由 `plugins/pencil-flight/` 中的 Lua 场景和 Skia 渲染实现。下文原生绘制性能与验收数据为迁移前的历史记录,旧工具已移除。当前回归入口为 `sh scripts/test-plugins.sh`、`zsh scripts/test-plugin-macos.sh` 和 `python3 tools/plugins/preview.py`。 - -更新:2026-10-23。 - -## 使用 - -`zsh build.sh` 构建 `dist/Miucs.app`。默认打开 400 × 260 透明悬浮画面,鼠标穿透、不接受键盘焦点。菜单栏波形图标可显示、隐藏、重新连接声音,或切换「调整位置与大小… / 完成调整(恢复鼠标穿透)」。菜单栏「画面大小」提供 10%、80%、100%、125%、150%、200% 预设;调整模式下也可拖动右下角连续缩放。始终保持 400:260 比例,宽度限定在 40~1000 点且不超出当前屏幕,大小与位置使用明确的坐标和尺寸记录保存,移动后自动更新,窗口关闭或应用退出时补存;下次启动优先恢复该记录,兼容旧版窗口自动保存数据。恢复过程不会用初始默认位置覆盖记录,显示器移除时会把画面限制到当前可见屏幕。隐藏时退出调整模式并停止绘制定时器。 - -画面使用原生 AppKit 路径绘制,石墨灰主线、极细浅色衬线,无底色、卡片或窗口阴影。云、地面、鸟、管道和提示统一为线稿,数字使用系统手写字体。笔迹扰动按物体 ID 固定,不在每帧重采样。动画以 60 fps 更新。可靠音高到达时立即设置运动目标,下一帧开始升降;无声且运动已收稳时跳过重复绘制。管口笔迹种子只取自物体 ID 与上下位置,不随开口高度变化。 - -## 音频与表现 - -| 输入 | 表现 | -| --- | --- | -| 主旋律音高 | 即时设置鸟的目标高度,短缓动跟随;近处管道提前移动并柔和展开开口 | -| 起音事件 | 一次振翅和一笔羽毛尾迹;管道独立随机生成 | -| 音量包络 | 鸟的整体体积、翼幅与身体伸展幅度,不改变高度 | -| 频谱带宽 | 新管道的水平出现距离,频谱越宽越远;已有管道继续原来的前进 | -| 频谱变化量 | 鸟后冒出膨胀、后飘并淡出的线稿放屁气团;独立于起音布尔事件 | -| 低频能量 | 管道线条粗细和管口回弹,始终留在保留的碰撞包络内 | -| 中频能量 | 前进速度在活动基速附近变化 | -| 高频能量 | 细短尾迹数量 | -| 频谱重心 | 背景线条轻重,采用约 1.2 秒平滑 | -| 频谱平坦度 | 尾迹分散程度 | -| 静音 / 数据超时 | 停止写入管道、收翅、逐渐减速至精确停止 | - -基础测量与独立 `MelodyFrame(time, pitchHz?, confidence, isMelodic)` 通过 `FeatureChannel` 交给可视化。绘制按屏幕刷新读取最新快照、起音事件与频谱变化峰值,不逐帧订阅 `AudioEngine`。主旋律没有用频谱重心、最大 FFT 频点或音量代替。模块与平台边界见 [架构说明](architecture.md)。 - -鸟后方随鸟一起升降的手写标签显示当前可靠主旋律的绝对音名与八度:C4 为中央 C,A4 为 440 Hz,升半音用 ♯ 表示。标签直接来自音高估计,不从鸟的缓动位置反算,因此不会被自动音域适配改变音名。它表示最近的十二平均律音符,不推断歌曲调性或相对唱名。音符边界采用 15 音分滞回避免颤音时频繁闪换;短暂未检出时保留并淡出,240 ms 后显示淡色「—」,静音或断开立即隐藏旧音名。 - -## 音频自动恢复 - -采集连接与应用的监听意图分开管理。休眠或屏幕关闭时停止当前流并取消重试,唤醒后等待约 0.7 秒重新查询显示器、建立新流。输出设备切换、显示器变化和用户会话恢复也会触发重建;短时间重复通知合并处理。 - -采集错误会按 1、2、4、8、16、30 秒逐步延长间隔自动重试,之后最多每 30 秒一次;持续收到正常数据 20 秒后恢复短间隔。每两秒检查一次数据健康度:默认输出设备正在运行且超过 12 秒没有有效音频数据时自动重建。静音 PCM 仍算正常数据,输出设备空闲时不以无数据判定故障。连续八次解码或分析失败也会重连。 - -每次连接拥有独立标识和分析器,旧流的迟到数据、错误或启动完成都不能覆盖新连接。主动停止或退出会取消重试并移除观察者;macOS 捕获控件中的主动停止也会得到保留。授权不足时仅定期检查授权状态,授权恢复后自动连接,不反复请求捕获。菜单栏保留「重新连接音频」手动入口。 - -恢复测试使用可注入采集器和真实异步重试任务,覆盖中断、退避、休眠/唤醒通知、设备变化合并、静音与数据卡死、授权恢复以及旧连接竞态。另用真实 ScreenCaptureKit 流验证内部休眠/唤醒与配置重建后继续交付音频;测试没有让电脑实际休眠或切换物理设备,报告仅存检查结果。 - -## 主旋律提取及明确限制 - -新增纯 Swift / Accelerate 的因果近似提取器,4096 点 Hann 窗、1024 样本步长。输入保留各声道功率,反相不抵消。对插值后的谱峰做 10 音分分辨率的谐波显著性投票、子谐波抑制与短时轮廓连续性选择;可靠音高直接发布,不额外等待第二帧确认大跳;候选选择仍保留短时连续性约束。支持范围约 E2…C7;静音跳过 FFT。 - -2026-10-24 加入有上限的局部频谱白化和强峰对比权重,保持相同窗口、步长及因果响应。已用《童话》的真人混音、独立参考谱和人工音高做对照,并记录其他音乐的回归及退步案例,见[主旋律识别对照](melody-benchmark.md)。 - -这不是完整的 MELODIA 实现,也不是训练好的音乐理解模型。`confidence` 是启发式置信指标,不是校准概率。它倾向于选择显著、连续的谐波主线,但无法保证在所有复音音乐中选择人耳认为的主旋律;强伴奏、复杂歌剧和密集编曲可能误选。人声合成测试不等于真实歌唱验证,因此另外跑了 ADC2004 的真实/合成标注片段,逐曲结果完整保留。 - -高度采用半音坐标,初始 C3…C6。最近 30 秒有效音高的 10%/90% 分位各外扩 2 半音,音域跨度至少一个八度;有至少 3 秒、24 个有效样本后开始适配,边界每秒最多移动 0.7 半音。可用飞行区上下各留出约 15% 安全空间。音高缺失时保持最后高度,首次无音高为中间高度。音高输入到达后直接设置目标高度;使用保持速度连续的临界阻尼运动收稳,不做路径预写或回放。 - -## 即时飞行与管道避让 - -已移除原先在前方写路线、鸟延后经过的机制,因此不再有随场景滚动产生的约 2~3 秒等待。当前可靠旋律立即设置鸟的目标高度,下一动画帧开始运动。临界阻尼缓动不跳位置、不弹跳:从静止切换音高时,150 ms 内完成超过 95% 的高度变化;小幅音高抖动也会被自然过滤。鸟身倾角随实际升降速度平滑改变。管道不会反过来限制鸟的高度。 - -管道的前方出现距离由频谱带宽决定:0~5000 Hz 的带宽线性映射到鸟前方 200~290 单位,超出范围限幅,缺失时回到 250 单位;约 0.45 秒时间常数平滑过渡。已有管道不被重新定位。管道中心在 72~180 随机生成,开口半高 40~48。有声音时按 74~90 单位的基础前进间隔尝试生成(动画离散步长可多出约 1 单位);带宽减小时延后生成以保证相邻管道至少相距 74 单位,带宽变化期间间距也可能增大。持续音不必等待起音。普通启动每次使用新随机种子,验证与预览使用固定种子。 - -距离鸟 220 单位以内,管道就开始以较慢的临界阻尼运动提前避让;使用鸟的当前运动趋势做约 45 ms 外推,90 单位内完全进入保护状态。管口用圆滑的安全包络,覆盖鸟体上下各 14 单位余量:大跳音时开口柔和展开,随后收回正常宽度,不再瞬间硬移开口。鸟的整体缩放为 0.8~1.4 倍,由现有音量包络映射并以约 0.12 秒时间常数平滑,不改变鸟的中心高度;这是数字信号强度的视觉映射,没有新增 LUFS 响度测量。避让与通过计分使用包含缩放、身体伸展和旋转的实际轮廓余量,保护持续到管道整个宽度越过鸟嘴、尾巴,之后逐渐释放追随。鸟与管道在同一动画子步更新,因此整个过程中都保持可穿行;输入到达本身不会让鸟或管道瞬移。远处管道保持生成时的高度,每根只加一分。笔触误差仍固定在物体上,管道移动不会重新随机生成笔迹。管道整层(含浅色衬线和排线)按水平位置从画面右缘的完整可见度渐变到鸟所在竖线的零可见度;到鸟处及其左侧不再绘制。 - -频谱变化量超过 0.04 的部分映射为 0~1 的气团强度,变化越强,线稿气团越大、越明显。每次有效音频帧的短峰值保留到下个动画子步;触发最小间隔 0.14 秒,同时最多六团,0.5~0.75 秒内向后飘、膨胀并淡出。轮廓复用固定路径,使用飞行尾迹笔色,不增加声音效果。静音或断开取消待触发峰值,已有气团自然消散;不重放旧音频数据的变化量。 - -48 kHz 下旋律窗口覆盖约 85 ms,输出间隔约 21 ms。用每次 10 ms 的 PCM 块输入,测量音符切换到第一帧正确新音高可用的时间,三个受控跳进分别为 70 / 50 / 50 ms,原实现为 200 / 160 / 160 ms。这是基于样本交付时刻的分析响应测试,包含窗口与步长等待,不包含实际计算耗时、系统音频采集、主线程调度、显示刷新与新增的短运动缓动;尚未测量真实声画端到端延迟,不能称为物理零延迟。 - -## 铅笔配色 - -菜单栏「线条颜色」直接提供石墨灰、雾蓝、苔绿、赭橙、雾紫五种笔色。小鸟、管道、音名、背景与尾迹使用同一主色,原有线条轻重和浅色衬线继续保留。与五线谱及未来模块共用一个选择,详见[公共笔色](appearance.md)。 - -`VisualizationAppearanceStore` 将全局颜色保存为一个版本化 ID;启动和切换模块时由外壳应用。颜色变化只更新 CGColor 与文字缓存,不重建飞行场景,笔迹路径缓存继续复用。首次升级按旧小鸟主色选择最近的预设,原始 `PencilPaletteStore` 配色数据保留。 - -`zsh scripts/test-appearance.sh` 使用隔离偏好验证菜单点选、颜色记忆、迁移、会话切换与透明度。插件绘制通过 Skia 预览和运行库测试验证。 - -## 绘制性能 - -动画改为 macOS 14 的视图级 `CADisplayLink`,按当前屏幕刷新回调推进,目标 60 fps,使用显示时间戳计算运动步长。去掉定时器到主线程异步任务的额外跳转,重复显示不会建立多个刷新源;隐藏或移出窗口时停止,重新显示时恢复。 - -绘制使用 Core Graphics 路径,笔迹按种子和局部形状复用,平移不重新生成路径。缓存只保存每个种子的当前形状,上限 512 项;文字复用 Core Text 排版,上限 64 项,分数增长不会无限占用内存。浅色衬线、手写字、透明底与音高响应均保留。 - -管道仍按右侧到鸟位置的同一渐变消失,但只在每根管道的 60 单位窄区域做透明合成。管道彼此不重叠,窄区域包含线宽和笔触扰动余量,避免每帧合成整个右侧区域。 - -性能探针使用固定种子的合成场景,分别记录 400、800、1000 点宽、2 倍像素密度下的离屏绘制;另显示真实原生透明窗口,预热一秒后测六秒。原生窗口记录的是主线程绘制提交时间与绘制间隔,不包括 GPU 合成完成时间;离屏与原生结果均受机器负载影响,不代表所有歌曲与桌面的帧率保证。对比记录保留全部尺寸和测量边界:[性能记录](validation/render-performance.json)。合成静帧重复绘制保持一致,前后画面比较使用预乘 RGBA,透明像素不会造成虚假差异。 - -本机 1000 点原生窗口的平均绘制提交时间约从 0.78 ms 降至 0.51 ms,绘制间隔的第 95 百分位从约 18.64 ms 降至 16.83 ms;恢复显示后的一秒记录到 60 帧。离屏结果中,400 点未见改善、800 点接近持平、1000 点平均绘制耗时降低约 27%,因此只将大尺寸绘制与原生提交开销的下降作为本轮测量结论。 - -```sh -``` - -后一个命令短暂显示合成测试窗口,验证隐藏停止、再次显示恢复。探针不读取真实屏幕或系统声音,不改动用户保存的位置和大小。 - -## 验证记录 - -| 检查 | 结果 | 记录 | -| --- | --- | --- | -| 分层与实时通道 | 16/16;最新快照、64 事件容量、250ms 过期、峰值保留、并发、重连隔离、会话切换、格式变化与陈旧帧;6 份合成样本及 Swift 往返通过 Schema | [契约测试](validation/contract-tests.json) | -| 通用分析编排 | 3/3;逐值保持原有编排结果,格式变化不沿用旧旋律,异常输入可恢复 | [编排测试](validation/audio-pipeline-tests.json) | -| 原生模块宿主 | 11/11;旧会话与设置释放、视图替换、音频不中断、窗口几何/透明/穿透保留、宿主接管调整操作 | [宿主测试](validation/visualization-host-tests.json) | -| 基础音频回归 | 28/28,通过原有 542 窗口 NumPy 参考 | [基础测试](validation/audio-feature-tests.json)、[独立参考](validation/numpy-reference.json) | -| 主旋律受控测试 | 14/14;新增因果响应时间测量;单音、缺失基频、八度跳进、带合成伴奏的器乐/共振峰音源、音量不变性、宽带噪声拒绝、分块一致性 | [测试结果](validation/melody-tests.json) | -| 主旋律 ADC2004,2026-10-22 基线 | 20 段,平均 50 音分内原始音高准确率约 74%;逐曲约 36%~98%,不能当成所有歌曲都通过 | [历史逐曲结果](validation/melody-adc2004.json) | -| 真实歌曲及最新 ADC2004 对照,2026-10-24 | 《童话》另外两位演唱者 77.59% → 79.21%;ADC2004 全集 73.65% → 75.60%,仍有退步片段和明显伴奏误报 | [完整对照](melody-benchmark.md) | -| 世界规则 | 15/15;音名与即时跟随、音量缩放、带宽与出现距离、快速带宽变化下的管道间距、独立气团与静音失效;3 分钟极端跳进并交替改变体积和带宽,26467 次重叠检查、0 次间隙违规、0 次输入瞬移 | [规则测试](validation/flight-world-tests.json) | -| 新特征绘制 | 原生 1000 点透明窗口合成场景约 59.3 fps;隐藏后停止,重新显示后一秒恢复 58 帧;15 项世界规则与 25 项配色检查通过 | [刷新记录](validation/feature-mapping-render.json)、[配色检查](validation/pencil-palette-tests.json) | -| 原生窗口 | 19/19;新增移动后保存、未完成调整时退出保存、重开恢复位置及屏幕变化与无效记录恢复;透明、不抢焦点、鼠标穿透;比例缩放、视图跟随尺寸、大小保存恢复、屏幕边界和调整模式 | [窗口测试](validation/flight-window-tests.json) | -| 系统播放/采集 | 12/12 原特征片段通过;低音、主音、反相信号均能输出正确旋律候选 | [基础链路](validation/system-audio.json)、[旋律链路](validation/melody-system-audio.json) | -| 音频自动恢复 | 18 项恢复规则与竞态测试;真实系统采集重建 4 项 | [恢复规则](validation/audio-recovery-tests.json)、[真实采集恢复](validation/audio-recovery-system.json) | -| 绘制检查 | 深浅背景均已检查,导出的透明场景约 93% 像素 alpha=0,边角透明 | `build/flight-previews/` | - -ADC2004 评估采用分析窗中心时间、最近参考标注时间、50 音分容差,报告未检出帧、错误旋律与逐曲表现。数据只放在忽略的 `build/adc2004/`,不随应用打包。2026-10-22 缩短分析窗后重跑全部 20 段,平均准确率由约 71% 变为约 74%;受控响应提速未造成这组数据上的整体退化。2026-10-24 的最新对照记录在上表单独链接中,不覆盖历史报告。报告不是未见数据上的质量保证。 - -本次目录与实时通道重构后,透明/浅色/深色三张预览与迁移前逐像素一致;两支合成 GIF 均经新工具入口重新导出。解锁桌面后的 1000 点宽原生窗口测量约 59.7 fps,绘制间隔 p95 约 16.88 ms,主线程绘制提交 p95 约 0.75 ms;隐藏后停止,重新显示后一秒恢复 60 帧。真实采集 12/12 片段与恢复 4/4 已复验通过。已重启新构建,尺寸与配色保留,位置按当前屏幕可见区域恢复。离屏耗时与原生提交耗时均不代表 GPU 合成完成时间。此次检查汇总见 [分层验收记录](validation/architecture-validation.json)。 - -## 复现与调试 - -```sh -zsh scripts/test-melody.sh -zsh scripts/test-audio-recovery.sh -zsh scripts/validate-audio.sh -zsh build.sh -``` - -实际播放验证会短暂播放低幅度测试音: - -```sh -zsh scripts/validate-audio.sh --live -``` - -播放声音时,可验证真实采集流恢复(只记录通过/失败,不保存声音特征): - -```sh -dist/Miucs.app/Contents/MacOS/Miucs --verify-audio-recovery --output "$PWD/docs/validation/audio-recovery-system.json" -``` - -若本地已有 ADC2004 WAV/REF 文件: - -```sh -python3 scripts/evaluate-melody.py build/adc2004 -``` - -预览命令明确使用合成的音频特征,只用于绘图验证;普通应用启动始终读取系统音频。应用不保存真实 PCM、屏幕画面或任何语音内容。运行时没有网络调用、外部模型或 Python 依赖。 - -参考:[谐波显著性定义](https://essentia.upf.edu/reference/std_PitchSalienceFunction.html)、[主旋律提取背景](https://www.justinsalamon.com/melody-extraction.html)、[ADC2004 数据来源](https://www.ee.columbia.edu/~dpwe/LabROSA/projects/melody/)。 diff --git a/docs/staff-note.md b/docs/staff-note.md deleted file mode 100644 index 1669143..0000000 --- a/docs/staff-note.md +++ /dev/null @@ -1,135 +0,0 @@ -# 五线谱音符 - -![五线谱音符:工作台合成音乐与打字输入预览](assets/plugins/staff-note.gif) - -工作台使用统一合成输入生成的 12 秒循环 GIF,无声。查看[全部插件预览](plugin-previews.md)。 - -当前可视化由 `plugins/staff-note/` 中的 Lua 场景和 Skia 渲染实现。下文原生绘制性能与验收数据为迁移前的历史记录,旧工具已移除。当前回归入口为 `sh scripts/test-plugins.sh`、`zsh scripts/test-plugin-macos.sh` 和 `python3 tools/plugins/preview.py`。 - -`staff-note` 是第二个内置可视化模块,菜单名为「五线谱音符」。它消费进程内特征通道,音高、起音、音量、节拍和键盘输入同时驱动画面。Windows 使用同一 Lua 插件,平台状态见 [Windows 说明](../apps/windows/README.md)。 -由混音主旋律驱动:歌声和主奏乐器都可被选择,保留可靠性与连续性判断。当前配对结果见[混音主旋律](predominant-mixture.md)。 - - -## 画面与音乐映射 - -基准画面 800 × 160,5:1 比例;宽度范围 80~1600 点,同时受屏幕可见区域限制。实心符头中心固定在 x=640,作为符干和符尾左右倾摆的支点。透明、无边框、鼠标穿透;默认石墨灰,菜单可选择[公共五种笔色](appearance.md),保留浅色衬线,笔迹的弯曲与排线位置固定。 - -保持高音谱号和 8 点线距,五条线是 y=58、66、74、82、90。相邻自然音相隔 4 点,八度相隔 28 点;半音统一采用升号拼写,同一自然音与其升音同谱位,由 ♯ 区分。当前只跟随真实音高 G2~C6,超出此音域的候选直接忽略,不压到边界。混音频谱分析器覆盖 E2~C7,两个可视化模块共享其可靠音高标记。 - -共享分析器的音高选择与显示谱位是两个环节。真实歌曲的识别对照及误报边界见[《童话》基准](melody-benchmark.md);该优化不会改变同一音高的固定谱位。 - - -谱位由真实音高直接确定:B4 固定在中线 y=74,G4 固定在 y=82,C4 固定在 y=98。相同音高在不同乐段、静音恢复和重新连接后都使用同一目标高度;只有接受新的音高时,符头才平滑移到新谱位。持续音不会自动居中、折叠八度或因近期旋律的平均音高而漂移,已有轨迹也不会整体上下移动。 - -音名始终显示实际音高,高音谱号保持固定,不显示动态 8 / 15 八度标记。五线谱和键盘星爆位置保持固定。只绘制五条主线,取消高低音的额外加线,避免符头附近出现一叠短横线。这里的 y 向下增加。 - -| 特征 | 表现与限制 | -| --- | --- | -| 主旋律音高 | 跟随范围 G2~C6;置信度至少 0.48,且 `isMelodic` 为真;半音量化并保留 15 音分边界滞回。半音/全音立即更新,较大变化经 100~150ms 连续确认后更新目标,临界阻尼弹簧约 150ms 收稳 | -| 起音事件 | 角速度脉冲触发符干和符尾绕实心符头中心左右倾摆,连续起音交替施力,角度连续;每侧最多倾斜 20°,约半秒一个回摆周期,阻尼振荡后回正;力度随音量包络和起音强度增加,符头中心保持准确谱位,符头形状、升号和音名不随倾摆旋转 | -| 节拍事件 | 原始混音上的实时节拍跟踪,在随机落点展开两层轻弧水波,带三道小弧线散开;主波最大直径约 64 点,整组约 96 点范围、890ms 内消失,力度轻微影响透明度,音符谱位保持不变;无稳定节拍或静音时不新增 | -| 音量包络 | 按当前乐段的短时分贝范围增强缩放对比,围绕符头中心平滑缩放至约 0.8~1.5 倍,静音回到 0.94 倍;范围向外立即扩张、向内以 3 秒时间常数收敛,最小动态跨度 12dB,稳定长音保持稳定;原始音量包络仍增强起音摆动力度,准确谱位与轨迹不受缩放影响 | -| 可靠旋律历史 | 保留过滤后音名及真实谱位,线条使用 25ms 时间常数轻度平滑;向左每秒移动 70 点、线性淡出,最多 8 秒/512 点 | -| 无有效基频、越界或待确认旋律 | 明确无基频立即停止末端连接、淡出音名并停止新增倾摆;不追加旧音高。音乐持续播放时,后续真实点接回原轨迹,模型缺失和换音不会切线;没有新有效音高达 240ms 也停止末端连接,最后谱位保持不动 | -| 静音、失联或过期音频 | 停止新倾摆,已有角度平滑回正;已有轨迹自然移动、过期 | - -首次没有旋律时,在中线以低透明度待机。静音或失联时切开轨迹;持续有声期间,音高过滤本身不再切段。可靠点仍按 8 秒过期,全部过期后末端一并消失,不留下永久横线。帧龄达 400ms 视为过期音频;重连、格式变化、PCM 时间线重置及隐藏恢复清理旧输入历史。新模块或恢复显示使用新通道订阅,旧起音不补播。 - -造型固定为普通八分音符;不根据音乐推断时值、调号、拍号或和弦,不生成完整乐谱。新模式不显示飞鸟的管道、计分、云朵、气团或调色界面。 - -## 拍点圆环 - -原始混音经 `BeatTracker` 跟踪 50~200 BPM 的周期与拍点,通过独立 `beats` 队列触发。每拍随机选择一个落点,两层有留白的主弧相隔 90ms 展开;三道小弧线分别延迟 100、155、210ms 出现,向不同方向轻轻散开。弧线两端淡化,保留浅色衬线和全局笔色,整体略扁(纵向 0.9 倍)。相邻拍点横向至少间隔 120 点,不跟随音符。波纹绘制在音符和音名下方,上下边缘柔和渐隐。首次锁定需要几秒,弱节奏可能不触发。缓存弧线路径与三档端点透明度,复用二十个原生波纹槽,最多保留四次拍点,复用屏幕刷新;静音、失联、隐藏或切换时清空,不补播旧拍点。算法、时间语义与验证见[节拍识别说明](beat-tracking.md)。 - -## 键盘五角星 - -键盘和音频同时驱动画面:键盘不影响音高、倾摆、缩放与旋律轨迹;音频静音、未授权或重连也不会清空已出现的星星。没有按键时不显示键帽、键名或键盘轮廓。 - -`StaffKeyboardLayout` 使用 ANSI QWERTY 主键区的物理位置。每行宽 15 个键宽单位,横向拉伸到 x=80~760,各行中心 y=36、58、80、102、124。数字行退格占 2 单位,Tab / 反斜杠占 1.5,Caps Lock 占 1.75,回车占 2.25,左右 Shift 分别占 2.25 / 2.75,因此 Q、A、Z 保留真实错位。底行从左到右是 Fn、Control、Option、Command、空格、右 Command、右 Option、右 Control;空格占 6.25,其余各占 1.25。爆点使用宽键中心。 - -macOS 的虚拟键码由 `MacStaffKeyboardLayout` 转成通用键位,不读取字符,输入法不改变位置。首版不包含 F1~F12、Escape、方向键、导航区和数字小键盘。位置适配长条画布,五角星本身保持正常比例;窗口 80~1600 点宽时,位置和星形一起等比缩放。 - -| 输入 | 星爆效果 | -| --- | --- | -| 普通键首次按下 | 当帧出现中心五角星,100ms 内放大、淡出;6 颗半径约 3~5 点的小星向四周移动 12~28 点,轻微旋转,650ms 内消失 | -| 普通键长按 | 300ms 后首次补发,之后每 180ms 补发较小的中心星和 4 颗小星;忽略系统自动重复,松开立即停止新增 | -| 多键同按 | 各自位置独立爆散 | -| 左右修饰键、Fn | 仅真实按下触发一次;释放不会误触发,另一侧仍按住也能正确区分 | -| Caps Lock | 锁定和解除锁定各触发一次,相同锁定状态不重复触发 | - -星形轮廓沿用铅笔描边、浅色衬线及五种公共笔色。绘制顺序为五线谱 / 旋律轨迹 → 星星 → 拍点圆环 → 谱位标记 / 音符 / 音名,不添加背景、光晕或彩色发光。 - -`StaffKeyboardStars` 独立保存事件游标、长按节奏和有界粒子;`receive(keyboard:)` 暂存本帧快照,统一在音频更新中推进。最多 16 次星爆、112 颗星,超额回收最早一次。五角星路径预先缓存,原生绘制预建并复用 112 个矢量图层槽,不逐键创建计时器或主线程任务。 - -隐藏和切换模块清空星爆,恢复时重新订阅,不补播隐藏期间的按键。权限丢失、安全输入、监听代次变化及事件丢失会停止旧长按;已有粒子继续自然消失,恢复后只响应新按下。长帧按实际时间淘汰旧粒子,每个仍有效的长按最多产生一次当前补发,绝不循环追赶积压。无音频变化且没有存活星星或圆环时不再请求绘制。 - -自动检查覆盖键位、修饰键两侧同时按住、事件去重、长按、过期释放、权限/安全输入、重连、隐藏恢复、长帧和粒子上限;原生绘制覆盖五色、600/800/1600 点、浅深背景及音符前景层次。合成演示 GIF 同时展示音频与键盘,未采集或注入真实输入。验证摘要、CPU 数据与真实窗口刷新间隔见 [staff-keyboard-validation.json](validation/staff-keyboard-validation.json)。真实按键端到端验收与合成结果分开记录。 - -2026-10-23 的星爆版本通过 268 项自动检查,并在安装版中完成实体键盘的打字、长按和松开确认。每秒 20 次按键及满 112 颗星、三种尺寸的主线程 CPU 更新加图层提交 p95 为 0.044~0.317ms,低于 2ms 目标;使用线程 CPU 时钟,另行记录含调度/提交等待的墙钟时间。实际透明窗口在 800 点 / 15 秒及 1600 点 / 10 秒测试中平均约 59.86 / 60.00fps,最长间隔分别为 51.13 / 23.13ms,超过 25ms 分别有 33 / 0 次。CPU 达标不代表所有刷新都按时完成,完整波动数据已保留。 - -## 短时毛刺过滤 - -`StaffPitch.resolve` 将五线谱跟随范围收窄到 G2~C6(MIDI 43~84,按半音量化判断),不修改共享音频分析器或飞鸟模块。越界结果不产生边界音符,也不写入旋律采样历史。 - -`StaffPitchTracker` 只在五线谱模块中处理音高候选。首次可靠音高,以及静音、失联、重连或隐藏恢复后的首音立即采用;相对于最后接受的音高,变化不超过 2 半音时立即响应,保留正常人声的小幅换音和滑音。持续有声期间,没有新结果、越界或标签过期不会清除这个比较基准;明确的无可靠音高结果会清理候选上下文,下一次可靠结果按新起点处理。 - -| 变化幅度 | 连续确认要求 | 48kHz / 1024 样本步长下的合成输入实测 | -| --- | --- | --- | -| 3~5 半音 | 至少 2 个新结果,跨度至少 100ms | 约 107ms | -| 6~10 半音 | 至少 3 个新结果,跨度至少 125ms | 约 128ms | -| 至少 11 半音 | 至少 4 个新结果,跨度至少 150ms | 约 171ms | - -确认使用音频时间戳;重复快照不计数,结果间隔超过 75ms 时重新确认。待确认音高允许在最初候选附近上下 1 半音变化,远处的新候选重新计数。短暂偏离又返回原旋律会被丢弃;确认期间音符保持上一个可靠音高,停止追加采样。仅在有效音高仍新鲜时连接符头,不把被拒绝的候选写成真实音高。明确无基频立即关闭末端连接和新增倾摆,240ms 没有新有效音高也关闭末端连接;后续真实点按下节规则接回历史线。静音或采集失联才结束该段,恢复后开始新段。未确认候选不会刷新旧音高的有效期;越界、缺失或无效结果会清理待确认候选,静音、失联、格式变化和隐藏恢复还会清理音高比较基准。 - -轨迹采样单独按音频时间间隔做 25ms 的指数平滑,不包含音符本体的摆动、缩放或运动惯性。绘制时将末端 10 点范围平滑接到动画符头,避免音符移动中与轨迹脱开;这一视觉修正不改动采样历史。新段从真实谱位开始;持续音最终回到真实高度,保留八度距离。上述确认耗时从首个新候选算起,不包括采集、音高分析和显示延迟;显示漏帧时还可能更长。 - -混音分析器先做能量、音调性和谐波支持度判断;上表的确认规则再抑制短暂误选。范围内持续占优的伴奏仍可能被确认,范围外的真实演唱音高和很短的真实大跳也会被过滤。回归使用合成音高序列,包括持续一至五帧的高置信度伴奏式插入、G2/C6 边界、连续越界、真实持续大跳、重复/稀疏输入、过期与断续音高,并检查小幅变化在首帧响应、异常期间轨迹不增加、恢复不绕过确认。结果和各跳跃确认耗时见 [staff-note-tests.json](validation/staff-note-tests.json)。 - -### 连续轨迹与显示置信度 - -显示层统一使用混音置信度门槛 0.48,结合 `isMelodic`、数值有效性、音域和换音确认。曾为歌声模型设置的独立置信度和短确认上下文已随模型移除;此前实验数据保留在验证目录。 - -明确无基频时,音名淡出、停止新增倾摆和末端连接,不延用旧音高。持续收到未静音的原始音乐时,后续接受的真实音高始终接回尚存的上一轨迹点;模型缺失、模型结果过期或音程变化都不再切断历史线。连接只发生在真实新点到达以后,缺失期间不增加轨迹点,也不会持续画出水平拖尾。对至少 80ms 且已停止末端连接的缺口,新点到达后采用水平切线的三次曲线,从两端逐渐生长,按缺口长短在约 190~300ms 内接拢;进度采用平滑缓入缓出,完成后保留同一条曲线。新音符正常响应,接线不等待模型下一帧。 - -补线计时从收到恢复结果的这一帧开始,不使用已经过了模型延迟的采样时间;重复快照不重播。已有音高点的时间和谱位保持固定,多个短缺口可独立完成连接,长帧直接跳到实际进度。真正静音、失联或隐藏仍按原有规则分段或清理。实现与原生绘图测试、连续缺口压测和刷新间隔记录见 [平滑补线验证](validation/staff-join-validation.json)。 - -音高确认状态和轨迹分段独立管理:明确无音高时清理候选上下文,不会因此切线;恢复帧仍须满足小于 240ms 的新鲜度要求。原始音频静音、超过 400ms 未更新、断开采集、格式或代次变化、隐藏或切换模块才会断开或清理轨迹。历史按实际时间在 8 秒后过期,全部过期后只显示下一次真实采样,不连接到已消失的旧点。 - -上一版只衔接 120ms / 5 半音以内的缺口,31 段回放仍有 270 段轨迹,未达到连续绘制的要求;该阶段记录保留在 [staff-neural-continuity.json](validation/staff-neural-continuity.json)。本次对相同缓存做 60Hz 刷新、120ms 模型延迟回放,31 段录音各保留一条连续历史线,原生矢量路径也各只有一个子路径。7884 个接受的音高点及时间戳与上一版逐点相同,变化仅在视觉连接方式。 - -回放用持续未静音的伴奏状态隔离模型缺失,不能代替实际采集或刷新间隔测量。歌声休止和识别失败都会被视觉连线跨过;它表达连续音乐中的音高走势,不表示连线经过的每一刻都检测到了歌声,也不向音频通道发布推测音高。结果、输入和源码哈希见 [staff-session-continuity.json](validation/staff-session-continuity.json),原生绘图对照见[上一版](validation/staff-session-continuity-before.png)和[连续版](validation/staff-session-continuity-after.png)。这属于旧人声版本的历史数据。当前插件使用固定数值回放验证连续性。 - -前一版范围与确认时间调整通过 161 项针对性检查,记录见 [staff-pitch-filter-validation.json](validation/staff-pitch-filter-validation.json)。上一版显示音区与短暂连接记录见 [staff-layout-continuity-validation.json](validation/staff-layout-continuity-validation.json)。恢复限制音域、保持有声轨迹连续并去除加线的记录见 [staff-contour-validation.json](validation/staff-contour-validation.json)。本次移除自动八度居中、固定谱位及保留轨迹连续性的回归与安装记录见 [staff-fixed-pitch-validation.json](validation/staff-fixed-pitch-validation.json)。 - -## 窗口与生命周期 - -飞鸟和五线谱分别保存位置、大小;应用记住上次选择,未知 ID 回退飞鸟。飞鸟继续读写原偏好键。切换先保存、暂停刷新,替换会话后恢复布局,再激活;音频连接不重启。首次切换保持左上角锚点,超出屏幕时回到可见区域。 - -## 绘制缓存与慢帧检查 - -原生窗口使用保留的矢量图层绘制五线谱、音符和轨迹;显示刷新只提交轨迹路径、位置、缩放和倾摆角度,音名变化时更新独立文字图层,减少整块画布重绘和位图提交等待。离线导出继续使用相同几何路径的 Core Graphics 参考绘制;平台验证覆盖原生图层的符头支点、旋转和五色透明输出。 - -符头、两种符干方向、升号和排线复用固定路径;原生图层与离线绘制均不再创建额外加线路径。全部 E2~C7 音名及待机横线在视图创建时准备文字样式和离线 Core Text 排版,换色时重建文字缓存。 - -音频看门狗在持续收到新 PCM 时直接判定采集健康;需要核对输出设备状态时,在后台执行 Core Audio 查询,返回后复核采集代次和取消状态。这样设备探测的同步系统调用不会阻塞界面刷新,旧探测结果也不会影响重连后的采集。 - -历史原生刷新测量使用合成输入和透明测试窗口;相关探针已随旧绘制器移除。当前性能测量使用插件 benchmark 和 surface probe,见 [插件迁移记录](plugin-migration.md)。 - -2026-10-23 对照测量中,640 点窗口的 15 秒刷新平均约 60fps,最长间隔从 61.29ms 降至 29.74ms;最终仍有 4 个超过 25ms 的间隔。1000 点窗口的 6 秒测量最长间隔从 49.69ms 降至 28.14ms。顺序测量受系统负载影响,不能据此保证所有场景无掉帧。各阶段数据、验证结果和对应源码哈希见 [staff-render-performance.json](validation/staff-render-performance.json)。 - -## 新增模块时的验证记录 - -```sh -zsh scripts/test-keyboard.sh -zsh scripts/test-contracts.sh -zsh scripts/test-visualization-host.sh -zsh build.sh -``` - -规则结果见 [staff-note-tests.json](validation/staff-note-tests.json),原生布局与绘图结果见 [staff-note-platform-tests.json](validation/staff-note-platform-tests.json)。测试仅使用合成音高与独立偏好域。合成预览覆盖 600/800/1600 点、透明/浅色/深色背景、G2/C6 边界与待机,重复静帧按像素检查笔迹稳定。 - -首次接入时共 206 项自动检查通过,独立 NumPy 参考 542 窗口无差异,飞鸟三张静帧与改动前字节完全相同。当时的构建和严格签名校验通过。解锁后原生刷新实测:五线谱约 59.5 fps,绘制 p95 为 0.43ms;飞鸟回归约 59.7 fps。两者隐藏后停止绘制,恢复显示后一秒均绘制 57 帧。测量使用 1000 点宽的真实透明窗口,6 秒内分别出现 1 次和 2 次超过 25ms 的刷新间隔,不代表长期无卡顿保证;源码哈希和性能记录对应首次接入版本。 - -原生应用已观察到从飞鸟切到五线谱的画面变化,并在新构建重启后恢复五线谱选择、位置与 800 × 160 尺寸;飞鸟原有位置和大小保持不变。修复了应用菜单未随模块刷新设置项的问题,五线谱下设置入口已正确禁用。缩放边界和模块切换生命周期由原生平台测试覆盖。汇总与对应源码哈希见 [staff-note-validation.json](validation/staff-note-validation.json),详细刷新数据见 [五线谱](validation/staff-note-cadence.json)与[飞鸟回归](validation/flight-cadence-staff-regression.json)。 - -原生 cadence 测量使用合成采集传输和真实透明窗口,需要显示器唤醒且桌面解锁;不捕获真实声音或屏幕。位图绘制耗时不代表原生呈现帧率。原有飞鸟绘制保持不变,回归对照使用同一合成场景。 diff --git a/docs/validation/README.md b/docs/validation/README.md index 75f2985..6854698 100644 --- a/docs/validation/README.md +++ b/docs/validation/README.md @@ -1,22 +1,13 @@ -# 验证记录索引 +# 验证记录 -此目录同时保存当前功能检查与历史实验快照。检查通过数只表示对应测试通过,不代表识谱准确率;合成 GIF 的导出帧率也不代表真实窗口刷新率。 +当前内置插件为奶牛猫、麦芬、牛来、平次与和叶。已移除插件的专属绘制和性能报告已清理;历史版本可从 Git 历史追溯。旧报告中的通过数和性能值只代表对应版本,不应当作当前构建的验收结论。 -2026-10-30 起只保留 Lua/Skia 插件;原生绘制、旧场景规则与刷新性能报告均为历史记录。当前插件检查入口见 [插件回归说明](../../tests/plugins/README.md),窗口和外观仍由宿主测试覆盖。 +## 当前检查入口 -## 功能与历史验证记录 +- [插件回归说明](../../tests/plugins/README.md):奶牛猫固定参考回放、其他皮肤规则、隔离传输、资源释放与热重载。 +- [贡献指南](../../CONTRIBUTING.md):音频、键盘、窗口、外观及双平台构建命令。 +- [插件运行时](../plugin-migration.md):Lua/Skia 架构、资源限制和平台验证。 +- [Windows 实机验收](../windows-acceptance.md):设备、DPI、输入权限、长时间运行范围。 +- [四个插件的 GIF 清单](../assets/plugins/manifest.json):统一合成输入导出的预览数据。 -- [移除人声分离依赖](remove-vocal-separation.json):清理后的构建、测试与混音输出等价检查。下方人声对照/安装记录保留的是移除前版本。 - -- [混音主旋律](predominant-mixture-validation.json):默认来源、51 项针对性检查、安装及重启;[31 段同音频对照](predominant-mixture-benchmark.json)、[编译结束后的串行性能复测](predominant-mixture-runtime.json)。算法准确性、声音目标变化及退步见[说明](../predominant-mixture.md)。 -- [轻弧水波](staff-beat-arcs-validation.json):最新选定的节拍视觉设计、29 项绘制/生命周期检查;[满载性能](staff-beat-arcs-performance.json)、[真实窗口刷新间隔](staff-beat-arcs-cadence-800.json)。 -- [平滑补线](staff-join-validation.json):真实音高恢复时的渐进连接,保留固定谱位与静音行为;[模拟规则](staff-note-tests.json)、[原生布局绘图](staff-note-platform-tests.json)。 -- [节拍识别](beat-tracker-tests.json)、[通道契约](contract-tests.json)、[音频特征](audio-feature-tests.json)、[音频管线](audio-pipeline-tests.json)、[采集恢复](audio-recovery-tests.json)。 -- [旋律规则](melody-tests.json);[人声门控规则](singing-pitch-tests.json)为已移除模型的历史快照。 -- [当前五线谱合成演示元数据](staff-beat-demo.json):对应 `docs/assets/miucs-staff-note.gif`,音频和键盘输入均为合成数据,不含用户录音或桌面。 - -## 历史记录 - -`vocal-energy-*` 记录人声相对能量门控;`staff-neural-continuity`、`staff-session-continuity*` 记录早期断线修复;`staff-beat-validation` 和 `staff-beat-compact-validation` 记录已被轻弧替代的圆环迭代。它们是对应阶段的证据,不表示当前默认来源或视觉设计。历史汇总中指向通用测试、性能或 GIF 文件的链接可能已经指向较新的结果;复核旧版本应以汇总内的时间、参数、源码/应用哈希和内嵌数值为准。 - -不同阶段的测试数量不可直接相加为“本次重跑数量”。性能记录区分线程 CPU、含调度等待的墙钟耗时和窗口刷新间隔;既保留通过目标的结果,也保留存在长帧的测量,不能据短测宣称所有桌面负载稳定 60fps。 +本目录保留共享音频、键盘、宿主和当前皮肤的历史验证数据。测试数量不可跨版本相加;GIF 导出帧率不代表真实窗口刷新率,短时测试不能替代持续内存、GPU 和跨屏实机验证。 diff --git a/docs/validation/appearance-validation.json b/docs/validation/appearance-validation.json deleted file mode 100644 index b38dbf0..0000000 --- a/docs/validation/appearance-validation.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "date": "2026-10-23T06:38:00.952119+00:00", - "scope": "Shared five-ink menu selection, persistent global appearance and native rendering adapters", - "status": "passed", - "choices": [ - "石墨灰", - "雾蓝", - "苔绿", - "赭橙", - "雾紫" - ], - "regressions": { - "passed": 183, - "failed": 0, - "reports": [ - { - "path": "docs/validation/visualization-appearance-tests.json", - "passed": 26, - "failed": 0 - }, - { - "path": "docs/validation/contract-tests.json", - "passed": 16, - "failed": 0 - }, - { - "path": "docs/validation/pencil-palette-tests.json", - "passed": 25, - "failed": 0 - }, - { - "path": "docs/validation/visualization-host-tests.json", - "passed": 11, - "failed": 0 - }, - { - "path": "docs/validation/staff-note-platform-tests.json", - "passed": 23, - "failed": 0 - }, - { - "path": "docs/validation/flight-world-tests.json", - "passed": 15, - "failed": 0 - }, - { - "path": "docs/validation/staff-note-tests.json", - "passed": 48, - "failed": 0 - }, - { - "path": "docs/validation/flight-window-tests.json", - "passed": 19, - "failed": 0 - } - ] - }, - "interactiveVerification": { - "method": "CUA inspected the actual app menu, clicked ink choices and observed recoloring, restarted and observed the same blue ink, then restored the original/migrated ink selection. No native UI screenshot was saved to the repository.", - "fiveChoicesVisible": true, - "menuSelectionUpdatesNativeDrawing": true, - "globalSelectionSurvivesRestart": true, - "moduleAndGeometryPreservedOnRestart": true, - "originalOrMigratedInkRestoredAfterVerification": true, - "legacyPaletteDataUnchanged": true - }, - "compatibility": { - "nativeBuild": "passed", - "strictSignatureVerification": "passed", - "sharedDependencyBoundaries": "passed", - "xcodeSourcePaths": "validated", - "audioAndJSONContractsUnchanged": true, - "noAdditionalAnimationTimer": true, - "offlineStoreLoadReadOnly": true - }, - "previews": { - "path": "build/shared-color-preview", - "syntheticOnly": true, - "modes": [ - "pencil-flight", - "staff-note" - ], - "inkCount": 5, - "lightAndDarkBackgrounds": true - }, - "pending": [], - "sourceSHA256": { - "apps/macos/App.swift": "c44db9dd294eed0c81add4c4d56ee463297c2221c87d641d2d8273b717b94faa", - "apps/macos/BuiltInVisualizations.swift": "fbc1879ee57edbf6d3b4335a523ae1405bb6f17f4d3755c0f0dc9552fd53435e", - "apps/macos/VisualizationAppearanceStore.swift": "48c79f04ce755aa175ac6de3a577179f3495bc659a317b081c42679e9e7a0bb3", - "apps/macos/VisualizationColorMenu.swift": "0d4c55ba66b0ae251eaee33a5840f4f5bf7521d818d3b7a738f5e09a4cbf6928", - "apps/macos/VisualizationHostView.swift": "217a0831604b39d74cdb4887b26f8be6b52ba87b0756b5de8ce5e5dadbc4ca34", - "apps/macos/VisualizerPanel.swift": "f8d74c37d9a692b22222f9b89df8174d013bc05de04c205fd57363f9ccfa9c48", - "modules/visualizations/api/VisualizationAppearance.swift": "f0e6a94458766202dc63f79867e47e4fe28aec4a7a4c7818631e419fc9d103ba", - "modules/visualizations/api/VisualizationModule.swift": "08095cd47bf528e6548f5625829c43929fced4d7ddd9834c021c8d6e99ddd555", - "modules/visualizations/api/platforms/macos/VisualizationColor+AppKit.swift": "6871fef23ca19b5366c14b259577c57828bb64d208f63f40078c0c9fa9fb3c57", - "modules/visualizations/pencil-flight/core/PencilPalette.swift": "3d58337890e721aeac1dd88553c04ab0b14d6e1efd9d9a098ae939937adad195", - "modules/visualizations/pencil-flight/platforms/macos/MacPencilFlightSession.swift": "51bb57dd7a7d74cf35efd0b6fbeec46a60891cdce09f94b52a54d36127c69e80", - "modules/visualizations/pencil-flight/platforms/macos/PencilPaletteStore.swift": "a69865fec012ed20cf4f63886a1e75d629b924c6c241cf886be58a82687bff13", - "modules/visualizations/staff-note/platforms/macos/MacStaffNoteSession.swift": "dc910a62ed85f0b41c5a344fffe958857f6dad444fff6d27a3f87d5af640ba59", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "65f1f51f8df0d367b18ccc78600de382be39730bd32b145f3f684ee0248947d5" - } -} diff --git a/docs/validation/flight-cadence-staff-regression.json b/docs/validation/flight-cadence-staff-regression.json deleted file mode 100644 index dbb41e5..0000000 --- a/docs/validation/flight-cadence-staff-regression.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "draw" : { - "maxMs" : 3.4489580430090427, - "meanMs" : 0.47509719138996226, - "medianMs" : 0.41829189285635948, - "p95Ms" : 0.89679146185517311, - "p99Ms" : 1.80395832285285 - }, - "fps" : 59.662269356129947, - "frames" : 358, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 53.268000017851591, - "meanMs" : 16.761011788386757, - "medianMs" : 16.669124830514193, - "p95Ms" : 17.372958362102509, - "p99Ms" : 21.244083531200886 - }, - "intervalsOver25ms" : 2, - "method" : "real native transparent window; synthetic capture transport; 1s warmup + 6s measurement; no recording", - "moduleID" : "pencil-flight", - "resumedFramesInOneSecond" : 57, - "widthPoints" : 1000 -} \ No newline at end of file diff --git a/docs/validation/flight-window-tests.json b/docs/validation/flight-window-tests.json deleted file mode 100644 index fa69e96..0000000 --- a/docs/validation/flight-window-tests.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "checks" : [ - { - "name" : "400x260 native panel", - "passed" : true - }, - { - "name" : "Transparent window, no shadow", - "passed" : true - }, - { - "name" : "Default mouse click-through", - "passed" : true - }, - { - "name" : "Scale to 150 percent without stretching", - "passed" : true - }, - { - "name" : "Drawing view follows window size", - "passed" : true - }, - { - "name" : "Scaling preserves mouse click-through", - "passed" : true - }, - { - "name" : "Custom size survives reopening", - "passed" : true - }, - { - "name" : "Native move saves position before finishing adjustment", - "passed" : true - }, - { - "name" : "Shutdown save restores unfinished adjustment and click-through", - "passed" : true - }, - { - "name" : "Small size has readable lower bound", - "passed" : true - }, - { - "name" : "Large size fits a visible display", - "passed" : true - }, - { - "name" : "Nonfinite size is ignored", - "passed" : true - }, - { - "name" : "Does not take keyboard focus", - "passed" : true - }, - { - "name" : "Position adjustment accepts mouse", - "passed" : true - }, - { - "name" : "Drag resize keeps upper left anchored", - "passed" : true - }, - { - "name" : "Finishing adjustment restores click-through", - "passed" : true - }, - { - "name" : "Hide restores safe input state", - "passed" : true - }, - { - "name" : "Disconnected display recovers frame into visible desktop", - "passed" : true - }, - { - "name" : "Malformed saved frame falls back safely", - "passed" : true - } - ], - "failed" : 0, - "passed" : 19 -} \ No newline at end of file diff --git a/docs/validation/flight-world-tests.json b/docs/validation/flight-world-tests.json deleted file mode 100644 index 0405436..0000000 --- a/docs/validation/flight-world-tests.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "checks" : [ - { - "metrics" : { - "C4" : 149.33333333333334, - "C5" : 102.66666666666667, - "E4" : 133.77777777777777, - "G4" : 122.11111111111111 - }, - "name" : "Pitch order, semitone mapping and octave preservation", - "passed" : true - }, - { - "metrics" : { - "maxRangeStep" : 0.017499999999998295, - "spanSemitones" : 12 - }, - "name" : "Adaptive range has an octave floor and bounded movement", - "passed" : true - }, - { - "metrics" : { - "distance" : 0, - "maxRemainingFractionAt150ms" : 0.047732532884316219, - "monotone" : true, - "noInputTeleport" : true, - "startsNextFrame" : true - }, - "name" : "Live pitch starts moving next frame and settles smoothly within 150ms", - "passed" : true - }, - { - "metrics" : { - "clearanceViolations" : 0, - "instantInputJumps" : 0, - "maxBirdStepAt60Hz" : 26.038848562195483, - "maxPipeEdgeStepAt60Hz" : 27.864349803202501, - "overlapChecks" : 26467, - "score" : 218, - "yieldingGates" : 220 - }, - "name" : "Random gates yield smoothly during extreme live pitch jumps", - "passed" : true - }, - { - "metrics" : { - "farGateChanges" : 0, - "generatedWithoutOnsets" : 29, - "maximumSpacing" : 90.378779660756891, - "minimumSpacing" : 74.22371778912111 - }, - "name" : "Far gates stay fixed; dense random spacing does not require onsets", - "passed" : true - }, - { - "metrics" : { - "heightDifference" : 0, - "loudScale" : 1.3999999999999979, - "maxScaleStepAt60Hz" : 0.033469832289817147, - "quietScale" : 1.0666666666666649 - }, - "name" : "Envelope gain changes bird size smoothly without changing height", - "passed" : true - }, - { - "metrics" : { - "narrowPipeLead" : 203.10343153312351, - "sameBirdHeightAndSize" : true, - "widePipeLead" : 289.50338210949616 - }, - "name" : "Spectral bandwidth controls pipe lead without changing pitch or bird size", - "passed" : true - }, - { - "metrics" : { - "maximumLeadStepAt60Hz" : 2.1067083105821496, - "minimumSpacing" : 74.068795293549783, - "score" : 82 - }, - "name" : "Rapid bandwidth changes preserve pipe ordering and minimum spacing", - "passed" : true - }, - { - "metrics" : { - "emittedPuffs" : 49, - "flapWithoutOnsets" : 0, - "maxConcurrentPuffs" : 5, - "strongestPuff" : 1 - }, - "name" : "Spectral flux emits bounded gas puffs independently of onset", - "passed" : true - }, - { - "metrics" : { - "firstBurstEmitted" : true, - "noSilentBurst" : true, - "staleBurstExpired" : true - }, - "name" : "Silence and missing frames cannot replay gas bursts", - "passed" : true - }, - { - "metrics" : { - "activity" : 0, - "distanceAfterStop" : 589.12026212929788 - }, - "name" : "Silence and disconnect coast to an exact stop", - "passed" : true - }, - { - "metrics" : { - "flap" : 1.0111519887139508e-31, - "heldHeight" : 122.11111111111111, - "score" : 11 - }, - "name" : "No melody retains height; stale onset is not replayed", - "passed" : true - }, - { - "metrics" : { - "actual" : [ - "C3", - "C4", - "C♯4", - "A4", - "B4", - "C5", - "C6" - ], - "expected" : [ - "C3", - "C4", - "C♯4", - "A4", - "B4", - "C5", - "C6" - ] - }, - "name" : "Trailing note preserves absolute name, accidentals and octave", - "passed" : true - }, - { - "metrics" : { - "boundaryHeld" : true, - "changedAtNewNote" : true, - "fades" : true, - "missingMelodyClears" : true, - "silenceClears" : true - }, - "name" : "Note label resists boundary chatter and expires on missing melody", - "passed" : true - }, - { - "metrics" : { - "particleCount" : 16, - "score" : 11 - }, - "name" : "Identical input gives identical scene", - "passed" : true - } - ], - "failed" : 0, - "passed" : 15 -} \ No newline at end of file diff --git a/docs/validation/heiji-kazuha.json b/docs/validation/heiji-kazuha.json new file mode 100644 index 0000000..ad4bc56 --- /dev/null +++ b/docs/validation/heiji-kazuha.json @@ -0,0 +1,28 @@ +{ + "checks": 3640, + "nodeCount": 268, + "swordFrames": 12, + "turnFrames": 8, + "slowFastSwings": [ + 8, + 17 + ], + "turnsAt60And120Bpm": [ + 2, + 6 + ], + "scriptP95Ms": 0.088542, + "scriptMeanMs": 0.040520081692690654, + "inputs": "synthetic, production Lua worker", + "nativeDesktopAcceptance": false, + "validation": { + "pluginBuild": "passed", + "fullPluginSuite": "passed", + "macOSMetalSurface": "passed, 148 frames", + "macOSCPUSurface": "passed, 148 frames", + "assetRegistration": "original RGBA bytes retained, SHA-256 verified", + "gif": "640x368, 12 seconds, 16 FPS sampling, 170 encoded frames", + "windowsDesktop": "not run locally; CI configured", + "macOSHostSelectionAndReload": "passed for all six skins" + } +} diff --git a/docs/validation/one-ui-per-plugin.md b/docs/validation/one-ui-per-plugin.md deleted file mode 100644 index 170a17f..0000000 --- a/docs/validation/one-ui-per-plugin.md +++ /dev/null @@ -1,16 +0,0 @@ -# One UI per plugin — 2026-10-29 - -The built-in package is split into `pixel-keyboard`, `pencil-flight` and `staff-note`. Each manifest has one top-level ID/name/layout and each folder includes its required scripts, fonts and images. Existing visualization IDs and window preference keys remain unchanged. Legacy multi-output manifests and plugin tray declarations are rejected with a migration message. - -The application owns the cat logo and tray animation. The visualization menu selects directly from the plugin catalog. Plugin scans do not launch workers; switching disposes the previous package; hiding the floating view stops plugin scheduling. Explicit reload can still validate a hidden selected instance. The tray has no Lua package or surface dependency. - -Local checks on macOS: - -- 37 native core checks, including three isolated built-in scenes, no tray outputs and rejection of the old manifest format. -- 68 process/transport checks and template lifecycle/resource validation. -- 600-frame Swift/Lua replay for each plugin, preserving keyboard head/tail, flight and staff/ring/path behavior. -- CPU previews for all three folders, plus native Metal staff and forced-CPU keyboard surface probes with no worker errors. -- macOS host regression: scans create no workers; selecting a UI creates only its package; disposal releases it and preserves the application tray. Failed/successful reload behavior also passes. -- Windows managed application cross-build and portable core tests pass. This is not a Windows native presentation run for this revision. - -Tests ran alongside compilation. Timing values from this run are not controlled performance acceptance measurements. The earlier implementation record remains in `plugin-runtime.md`; its single-package layout describes the pre-split version. Native Windows validation for this revision and the broader release gates remain separate from these local checks. diff --git a/docs/validation/pencil-palette-tests.json b/docs/validation/pencil-palette-tests.json deleted file mode 100644 index 10e1200..0000000 --- a/docs/validation/pencil-palette-tests.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "checks" : [ - { - "name" : "HEX accepts shorthand, case, optional hash, and outer whitespace", - "passed" : true - }, - { - "name" : "HEX rejects incomplete, alpha, non-ASCII, and malformed values", - "passed" : true - }, - { - "name" : "System grayscale colors convert to opaque sRGB ink", - "passed" : true - }, - { - "name" : "Invalid numeric components cannot enter the renderer", - "passed" : true - }, - { - "name" : "First launch preserves graphite", - "passed" : true - }, - { - "name" : "Preset applies and survives a fresh store", - "passed" : true - }, - { - "name" : "Independent color edit preserves all other parts", - "passed" : true - }, - { - "name" : "Custom colors survive restart and show their own name", - "passed" : true - }, - { - "name" : "Existing renderer follows palette changes synchronously", - "passed" : true - }, - { - "name" : "Recoloring keeps desktop transparency and click-through", - "passed" : true - }, - { - "name" : "Single-color mode updates and persists every ink", - "passed" : true - }, - { - "name" : "Reset restores drawing and removes saved override", - "passed" : true - }, - { - "name" : "Corrupted preferences safely fall back to graphite", - "passed" : true - }, - { - "name" : "Out-of-range persisted ink is rejected", - "passed" : true - }, - { - "name" : "Fixture contains notes, pipes, and particles", - "passed" : true - }, - { - "name" : "Actual bird pixels change after editing", - "passed" : true - }, - { - "name" : "Actual gates pixels change after editing", - "passed" : true - }, - { - "name" : "Actual notes pixels change after editing", - "passed" : true - }, - { - "name" : "Actual scenery pixels change after editing", - "passed" : true - }, - { - "name" : "Actual trail pixels change after editing", - "passed" : true - }, - { - "name" : "Reset invalidates cached text colors and restores exact pixels", - "passed" : true - }, - { - "name" : "Palette leaves the desktop background transparent", - "passed" : true - }, - { - "name" : "Palette is a separate keyboard-accessible window", - "passed" : true - }, - { - "name" : "Native palette UI renders offscreen", - "passed" : true - }, - { - "name" : "Closing the palette preserves edits and desktop geometry", - "passed" : true - } - ], - "failed" : 0, - "passed" : 25 -} \ No newline at end of file diff --git a/docs/validation/plugin-runtime.json b/docs/validation/plugin-runtime.json deleted file mode 100644 index 4e25b13..0000000 --- a/docs/validation/plugin-runtime.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "date": "2026-10-29", - "machine": "Apple M4", - "os": "macOS 26.6.2", - "configuration": "CMake Release / -O3; Swift -O; .NET Release", - "baselineCommit": "b6b888e", - "benchmark": [ - { - "decodeApplyP95Ms": 0.347667, - "frames": 600, - "meanPacketBytes": 230, - "output": "pixel-keyboard", - "peakLuaBytes": 561344, - "scriptP95Ms": 0.042 - }, - { - "decodeApplyP95Ms": 1.573625, - "frames": 600, - "meanPacketBytes": 11148, - "output": "staff-note", - "peakLuaBytes": 757946, - "scriptP95Ms": 1.612666 - }, - { - "decodeApplyP95Ms": 1.472916, - "frames": 600, - "meanPacketBytes": 6961, - "output": "pencil-flight", - "peakLuaBytes": 691447, - "scriptP95Ms": 1.402459 - } - ], - "replay": { - "frames": 600, - "maximumAbsoluteError": { - "head": 0, - "y": 0, - "scale": 0, - "opacity": 0, - "sway": 0, - "ring-x": 0, - "ring-y": 0, - "ring-opacity": 0, - "ring-rotation": 0 - }, - "roundTripP95Ms": 4.975541029125452, - "roundTripMeanMs": 2.789584833662957, - "scriptP95Ms": 1.864, - "scriptMaxMs": 4.005834 - }, - "soak": { - "durationSeconds": 1800.0322877909057, - "frames": 70177, - "workerGenerations": 6, - "status": "complete", - "note": "CPU raster soak ran alongside builds and during implementation updates; not the final-build GPU frame-drop or release soak gate." - }, - "pending": [ - "Windows native C++ build and real D3D12/DirectComposition acceptance", - "Cross-platform screenshot review", - "GPU completion timing and input-to-display latency", - "Final frozen Release build 30-minute runs on both target platforms", - "Default switch and legacy renderer removal" - ] -} diff --git a/docs/validation/plugin-runtime.md b/docs/validation/plugin-runtime.md deleted file mode 100644 index 8eb8bee..0000000 --- a/docs/validation/plugin-runtime.md +++ /dev/null @@ -1,38 +0,0 @@ -# Plugin implementation validation — 2026-10-29 - -Historical validation from 2026-10-29. As of 2026-10-30 all builds require the plugin runtime; native renderers and mode switches have been removed. The measurements below describe the implementation at the time of collection. - -## Completed locally - -Environment: Apple M4, macOS 26.6.2; CMake Release (`-O3`), Swift `-O`, .NET Release. The native application bundle builds and signs with the worker, shared library, built-in package, font and dependency notices. - -- 22 native core assertions: manifest/path validation, revision resync, atomic scene application, empty dynamic paths, deterministic seed, repeated dispose, node and aggregate scene budgets, restricted libraries, memory ceiling, bytecode and loop interruption. -- 68 process/transport assertions, including a shared path update invalidating the cached output, 50 queued updates, preserving input across an appearance control, a loop that catches Lua timeout errors, malformed/deep MessagePack, corrupt images a symlink escape and a missing host process. -- 600-frame production Swift reference replay for each built-in output. Head, tail atlas selection, staff position/scale/opacity/sway and ring positions/rotations match exactly; flight state maximum absolute error was `1.734723475976807e-18`. Trace command types, count and coordinates agree within `1e-8`. Fixtures include pitch dropouts, silence, a long frame interval, independent input reconnection and audio format change. -- Mac worker replacement test: syntax error keeps the old worker; a valid blue replacement renders correctly and receives the original viewport/scale. -- Native Metal and forced CPU window probes each drew 180 changing frames without worker errors. Metal draw/submit P95: **1.95 ms**; CPU P95: **6.20 ms**, after warmup at a 1000×640 backing target. These are CPU-observed draw/submission durations, not GPU completion times. -- Windows managed application cross-build and portable audio/keyboard/visualization/tray/preferences tests passed. This does **not** compile or execute the Windows native C++ presentation backend on macOS. -- Existing macOS visualization host and staff beat regressions passed. These retain the old implementations as a comparison baseline. -- Manifest schema, shared physical-key identities and dependency-boundary checks passed. Fn is explicitly macOS-only where Windows does not report an equivalent physical event. - -## Local performance sample - -600 frames per output, first 60 excluded, after this task's concurrent pressure workload finished. Binary decode and atomic scene application are measured together; scripts and scene submission are separate. - -| Output | Script P95 | Decode + apply P95 | Mean scene packet | -|---|---:|---:|---:| -| Pixel keyboard | 0.042 ms | 0.348 ms | 230 bytes | -| Staff note | 1.613 ms | 1.574 ms | 11,148 bytes | -| Pencil flight | 1.402 ms | 1.473 ms | 6,961 bytes | - -These local samples meet the 2 ms thresholds. Earlier measurements while compilation and CPU pressure ran concurrently were higher. This is a development-machine sample, not a controlled two-platform performance certification or a comparison against the old renderer's total GPU cost. - -A wall-clock 30-minute CPU raster stress run completed 70,177 frames, switching every 10 seconds and restarting the worker every five minutes. Combined Python host + worker RSS medians after each generation's first minute were approximately **125.3, 129.8, 132.8, 124.6, 118.6, 126.2 MiB**. This did not show sustained growth across generations. It ran alongside compilation and across implementation updates, so it is useful stress evidence, not the final frozen-build soak requirement. Its CPU pacing overruns are not a GPU dropped-frame measurement. - -## Required before switching defaults - -Windows native C++ compilation and physical-machine D3D12/DirectComposition validation remain unexecuted locally. The new CI workflow covers both platforms; its results must be checked independently of this local validation record. Transparency, mouse passthrough, WPF overlay ordering, monitor/DPI transitions and GPU-loss fallback need real Windows checks. - -Both platforms still need reviewed matching screenshots, GPU completion measurements, end-to-end input/display latency, a fixed-machine before/after comparison, <1% sustained dropped-frame verification, and final-build 30-minute GPU runs. macOS hardware sleep/lock/display-scale transitions also remain manual acceptance items. - -Consequently, step 4 (enable by default and remove old Swift/C# drawing/animation code) has intentionally **not** been performed. Implementation and validation commands are in `docs/plugin-migration.md`; machine-readable local measurements are in `plugin-runtime.json`. diff --git a/docs/validation/staff-beat-arcs-cadence-800.json b/docs/validation/staff-beat-arcs-cadence-800.json deleted file mode 100644 index 6906cad..0000000 --- a/docs/validation/staff-beat-arcs-cadence-800.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "draw" : { - "maxMs" : 1.1688750237226486, - "meanMs" : 0.18522367939105325, - "medianMs" : 0.14816690236330032, - "p95Ms" : 0.33079134300351143, - "p99Ms" : 0.48737507313489914 - }, - "drawsOver16_67ms" : 0, - "fps" : 59.998870024318308, - "frames" : 361, - "framesWithActiveJoins" : 375, - "framesWithRings" : 387, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 26.336874812841415, - "meanMs" : 16.666980554711902, - "medianMs" : 16.663041431456804, - "p95Ms" : 20.912791602313519, - "p99Ms" : 23.458624724298716 - }, - "intervalsOver25ms" : 2, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - { - "atSeconds" : 4.5776666249148548, - "currentDrawMs" : 0.13154186308383942, - "intervalMs" : 26.273875031620264, - "previousDrawMs" : 0.092208385467529297 - }, - { - "atSeconds" : 4.9778124997392297, - "currentDrawMs" : 0.10862527415156364, - "intervalMs" : 26.336874812841415, - "previousDrawMs" : 1.1688750237226486 - } - ], - "measurementSeconds" : 6, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 61, - "syntheticKeyboardPresses" : 164, - "widthPoints" : 800, - "withBeats" : true, - "withPitchGaps" : true -} \ No newline at end of file diff --git a/docs/validation/staff-beat-arcs-performance.json b/docs/validation/staff-beat-arcs-performance.json deleted file mode 100644 index a487dd2..0000000 --- a/docs/validation/staff-beat-arcs-performance.json +++ /dev/null @@ -1,327 +0,0 @@ -{ - "beatStress": "180 BPM during typing; four simultaneous artificial pulses every 500ms in capacity case to saturate all twenty main/small arc slots", - "burstLimit": 16, - "method": "600 measured synthetic frames after 9s warmup per size/pattern; production keyboard channel, native staff session and retained layer transactions; CLOCK_THREAD_CPUTIME_ID records main-thread CPU, systemUptime separately includes scheduling/commit waits; excludes WindowServer/GPU execution and presentation", - "p95BudgetMs": 2, - "passed": true, - "pitchGapStress": "In gap mode, four unvoiced frames in every ten create overlapping joins while new notes continue", - "ringLimit": 20, - "scenarios": [ - { - "animationUpdate": { - "maxMs": 0.032499898225069046, - "meanMs": 0.003864575022210677, - "medianMs": 0.0035832636058330536, - "p95Ms": 0.005749985575675964, - "p99Ms": 0.009291339665651321 - }, - "combined": { - "maxMs": 2.8853751718997955, - "meanMs": 0.11459909612312913, - "medianMs": 0.07895845919847488, - "p95Ms": 0.09287521243095398, - "p99Ms": 1.4771665446460247 - }, - "frames": 600, - "layerSubmission": { - "maxMs": 2.8528752736747265, - "meanMs": 0.11073452110091846, - "medianMs": 0.07520848885178566, - "p95Ms": 0.08899997919797897, - "p99Ms": 1.4731250703334808 - }, - "p95Under2ms": true, - "pattern": "typing20", - "peakActiveJoins": 2, - "peakRings": 11, - "peakStars": 87, - "threadCPUCombined": { - "maxMs": 0.19804100000000002, - "meanMs": 0.07147470666666668, - "medianMs": 0.06975, - "p95Ms": 0.080417, - "p99Ms": 0.097708 - }, - "threadCPUSubmission": { - "maxMs": 0.194625, - "meanMs": 0.06776359666666663, - "medianMs": 0.066208, - "p95Ms": 0.075916, - "p99Ms": 0.091083 - }, - "threadCPUUpdate": { - "maxMs": 0.032, - "meanMs": 0.0037111099999999997, - "medianMs": 0.003458, - "p95Ms": 0.005625000000000001, - "p99Ms": 0.007666999999999999 - }, - "wallP95Under2ms": true, - "widthPoints": 600 - }, - { - "animationUpdate": { - "maxMs": 0.01824973151087761, - "meanMs": 0.007428184617310762, - "medianMs": 0.007208436727523804, - "p95Ms": 0.00966666266322136, - "p99Ms": 0.011291354894638062 - }, - "combined": { - "maxMs": 2.789416816085577, - "meanMs": 0.12758993388464054, - "medianMs": 0.0899583101272583, - "p95Ms": 0.1043747179210186, - "p99Ms": 1.648416742682457 - }, - "frames": 600, - "layerSubmission": { - "maxMs": 2.781625371426344, - "meanMs": 0.12016174926732977, - "medianMs": 0.0827917829155922, - "p95Ms": 0.09595835581421852, - "p99Ms": 1.6411668621003628 - }, - "p95Under2ms": true, - "pattern": "capacity", - "peakActiveJoins": 2, - "peakRings": 20, - "peakStars": 112, - "threadCPUCombined": { - "maxMs": 0.149958, - "meanMs": 0.07501862000000004, - "medianMs": 0.08079199999999999, - "p95Ms": 0.090666, - "p99Ms": 0.10962500000000001 - }, - "threadCPUSubmission": { - "maxMs": 0.142042, - "meanMs": 0.06774375166666669, - "medianMs": 0.07375000000000001, - "p95Ms": 0.08354199999999999, - "p99Ms": 0.09816599999999999 - }, - "threadCPUUpdate": { - "maxMs": 0.018000000000000002, - "meanMs": 0.0072748683333333525, - "medianMs": 0.007083, - "p95Ms": 0.009541000000000001, - "p99Ms": 0.011125 - }, - "wallP95Under2ms": true, - "widthPoints": 600 - }, - { - "animationUpdate": { - "maxMs": 0.008624978363513947, - "meanMs": 0.003580283373594284, - "medianMs": 0.0034584663808345795, - "p95Ms": 0.004833098500967026, - "p99Ms": 0.006583519279956818 - }, - "combined": { - "maxMs": 2.3595415987074375, - "meanMs": 0.11280459119006991, - "medianMs": 0.07870839908719063, - "p95Ms": 0.0905417837202549, - "p99Ms": 1.3690418563783169 - }, - "frames": 600, - "layerSubmission": { - "maxMs": 2.355291973799467, - "meanMs": 0.10922430781647563, - "medianMs": 0.07529184222221375, - "p95Ms": 0.08649984374642372, - "p99Ms": 1.3650418259203434 - }, - "p95Under2ms": true, - "pattern": "typing20", - "peakActiveJoins": 2, - "peakRings": 11, - "peakStars": 87, - "threadCPUCombined": { - "maxMs": 0.096166, - "meanMs": 0.07043799666666659, - "medianMs": 0.069667, - "p95Ms": 0.077834, - "p99Ms": 0.082667 - }, - "threadCPUSubmission": { - "maxMs": 0.091541, - "meanMs": 0.06699736500000003, - "medianMs": 0.06629199999999999, - "p95Ms": 0.074042, - "p99Ms": 0.078959 - }, - "threadCPUUpdate": { - "maxMs": 0.008457999999999999, - "meanMs": 0.003440631666666666, - "medianMs": 0.003333, - "p95Ms": 0.0047079999999999995, - "p99Ms": 0.006417 - }, - "wallP95Under2ms": true, - "widthPoints": 800 - }, - { - "animationUpdate": { - "maxMs": 0.04079192876815796, - "meanMs": 0.007380275831868252, - "medianMs": 0.007166992872953415, - "p95Ms": 0.009499955922365189, - "p99Ms": 0.012541655451059341 - }, - "combined": { - "maxMs": 2.4063335731625557, - "meanMs": 0.12621436966583133, - "medianMs": 0.09074993431568146, - "p95Ms": 0.10649999603629112, - "p99Ms": 1.5442082658410072 - }, - "frames": 600, - "layerSubmission": { - "maxMs": 2.3991665802896023, - "meanMs": 0.11883409383396308, - "medianMs": 0.08354149758815765, - "p95Ms": 0.09858328849077225, - "p99Ms": 1.5369998291134834 - }, - "p95Under2ms": true, - "pattern": "capacity", - "peakActiveJoins": 2, - "peakRings": 20, - "peakStars": 112, - "threadCPUCombined": { - "maxMs": 0.164916, - "meanMs": 0.075255215, - "medianMs": 0.082, - "p95Ms": 0.09208300000000001, - "p99Ms": 0.1 - }, - "threadCPUSubmission": { - "maxMs": 0.150166, - "meanMs": 0.06806581666666664, - "medianMs": 0.074875, - "p95Ms": 0.08429199999999999, - "p99Ms": 0.09225000000000001 - }, - "threadCPUUpdate": { - "maxMs": 0.017333, - "meanMs": 0.007189398333333348, - "medianMs": 0.007042, - "p95Ms": 0.00925, - "p99Ms": 0.012333 - }, - "wallP95Under2ms": true, - "widthPoints": 800 - }, - { - "animationUpdate": { - "maxMs": 0.02658367156982422, - "meanMs": 0.0038299430161714554, - "medianMs": 0.003541819751262665, - "p95Ms": 0.005000270903110504, - "p99Ms": 0.012625008821487427 - }, - "combined": { - "maxMs": 3.035291563719511, - "meanMs": 0.12149041518568993, - "medianMs": 0.07933331653475761, - "p95Ms": 0.10704156011343002, - "p99Ms": 1.7619999125599861 - }, - "frames": 600, - "layerSubmission": { - "maxMs": 3.0125416815280914, - "meanMs": 0.11766047216951847, - "medianMs": 0.07575005292892456, - "p95Ms": 0.10362500324845314, - "p99Ms": 1.758416648954153 - }, - "p95Under2ms": true, - "pattern": "typing20", - "peakActiveJoins": 2, - "peakRings": 11, - "peakStars": 87, - "threadCPUCombined": { - "maxMs": 0.22124999999999997, - "meanMs": 0.07265409833333324, - "medianMs": 0.069917, - "p95Ms": 0.08479199999999999, - "p99Ms": 0.147 - }, - "threadCPUSubmission": { - "maxMs": 0.21779199999999999, - "meanMs": 0.06897999500000007, - "medianMs": 0.06658299999999999, - "p95Ms": 0.079083, - "p99Ms": 0.12774999999999997 - }, - "threadCPUUpdate": { - "maxMs": 0.026083000000000002, - "meanMs": 0.003674103333333337, - "medianMs": 0.003417, - "p95Ms": 0.004792, - "p99Ms": 0.012458 - }, - "wallP95Under2ms": true, - "widthPoints": 1600 - }, - { - "animationUpdate": { - "maxMs": 0.015916768461465836, - "meanMs": 0.007306129361192386, - "medianMs": 0.007125083357095718, - "p95Ms": 0.00966666266322136, - "p99Ms": 0.010791700333356857 - }, - "combined": { - "maxMs": 2.5387918576598167, - "meanMs": 0.12808633735403419, - "medianMs": 0.09020837023854256, - "p95Ms": 0.10533351451158524, - "p99Ms": 1.4861254021525383 - }, - "frames": 600, - "layerSubmission": { - "maxMs": 2.532291691750288, - "meanMs": 0.1207802079928418, - "medianMs": 0.08341670036315918, - "p95Ms": 0.09666662663221359, - "p99Ms": 1.4769169501960278 - }, - "p95Under2ms": true, - "pattern": "capacity", - "peakActiveJoins": 2, - "peakRings": 20, - "peakStars": 112, - "threadCPUCombined": { - "maxMs": 0.10425, - "meanMs": 0.07477811166666672, - "medianMs": 0.08116699999999999, - "p95Ms": 0.092042, - "p99Ms": 0.100333 - }, - "threadCPUSubmission": { - "maxMs": 0.09704099999999999, - "meanMs": 0.06762685833333335, - "medianMs": 0.074334, - "p95Ms": 0.08433299999999999, - "p99Ms": 0.090834 - }, - "threadCPUUpdate": { - "maxMs": 0.014083, - "meanMs": 0.007151253333333348, - "medianMs": 0.007, - "p95Ms": 0.0095, - "p99Ms": 0.010665999999999998 - }, - "wallP95Under2ms": true, - "widthPoints": 1600 - } - ], - "starLimit": 112, - "typingPressesPerSecond": 20, - "withBeats": true, - "withPitchGaps": true -} diff --git a/docs/validation/staff-beat-arcs-validation.json b/docs/validation/staff-beat-arcs-validation.json deleted file mode 100644 index 270f5ff..0000000 --- a/docs/validation/staff-beat-arcs-validation.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "date": "2026-10-26", - "design": "轻弧水波, selected interactive preview", - "mainLayers": 2, - "satellites": 3, - "groupLifetimeMs": 890, - "mainMaximumRadiusPoints": 32, - "ringLimit": 20, - "cachedTaperBands": 3, - "tests": { - "passed": 29, - "failed": 0 - }, - "performance": "staff-beat-arcs-performance.json", - "nativeCadence": "staff-beat-arcs-cadence-800.json", - "cpuP95MsRange": [ - 0.077834, - 0.09208300000000001 - ], - "demo": "staff-beat-demo.json", - "binarySHA256": "7a3b5017a53bcfea0ac0e68e27b3f6ea11098fafc6033933223f8a5656f21ec2", - "restarted": true, - "audioCaptureVerified": "48kHz stereo PCM after restart; Rapidly 96ms + CREPE Tiny", - "visualChecks": "Native 600/800/1600 point rendering, light/dark backgrounds and five inks; production-path synthetic GIF. Live window restored with note and trace. Subjective music trial remains with user.", - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffBeatRings.swift": "e2576b85823da1b232d49306fc3fc4bd7c9b5bb4d10c9a5530139de26bb6eb17", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "dd4a9bf38c3ebea6fc474bca7eb9456debec1904501abc888ab3e8b57c4a2bc2" - } -} diff --git a/docs/validation/staff-beat-cadence-800.json b/docs/validation/staff-beat-cadence-800.json deleted file mode 100644 index b3804ea..0000000 --- a/docs/validation/staff-beat-cadence-800.json +++ /dev/null @@ -1,730 +0,0 @@ -{ - "draw" : { - "maxMs" : 389.14337521418929, - "meanMs" : 1.0344000327435163, - "medianMs" : 0.22612512111663818, - "p95Ms" : 0.8652079850435257, - "p99Ms" : 4.0315417572855949 - }, - "drawsOver16_67ms" : 3, - "fps" : 46.980551486021362, - "frames" : 706, - "framesWithActiveJoins" : 610, - "framesWithRings" : 683, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 588.84929167106748, - "meanMs" : 21.285403605735471, - "medianMs" : 16.744791995733976, - "p95Ms" : 35.123374778777361, - "p99Ms" : 77.669417019933462 - }, - "intervalsOver25ms" : 116, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - { - "atSeconds" : 0.082326291594654322, - "currentDrawMs" : 4.0315417572855949, - "intervalMs" : 29.663249850273132, - "previousDrawMs" : 8.9077083393931389 - }, - { - "atSeconds" : 0.15809829160571098, - "currentDrawMs" : 0.71933353319764137, - "intervalMs" : 40.406833402812481, - "previousDrawMs" : 0.18520839512348175 - }, - { - "atSeconds" : 0.2015624581836164, - "currentDrawMs" : 0.16733352094888687, - "intervalMs" : 43.464166577905416, - "previousDrawMs" : 0.71933353319764137 - }, - { - "atSeconds" : 0.30547829158604145, - "currentDrawMs" : 0.25749998167157173, - "intervalMs" : 42.620374821126461, - "previousDrawMs" : 0.15154154971241951 - }, - { - "atSeconds" : 0.36114862514659762, - "currentDrawMs" : 0.24608336389064789, - "intervalMs" : 40.809125173836946, - "previousDrawMs" : 0.21933345124125481 - }, - { - "atSeconds" : 0.73699312517419457, - "currentDrawMs" : 0.17887493595480919, - "intervalMs" : 36.525208503007889, - "previousDrawMs" : 0.21520815789699554 - }, - { - "atSeconds" : 0.81266866670921445, - "currentDrawMs" : 0.19683316349983215, - "intervalMs" : 25.884666945785284, - "previousDrawMs" : 0.31279167160391808 - }, - { - "atSeconds" : 0.86361229186877608, - "currentDrawMs" : 0.21512480452656746, - "intervalMs" : 28.797667007893324, - "previousDrawMs" : 0.17675012350082397 - }, - { - "atSeconds" : 1.121067916508764, - "currentDrawMs" : 0.25454163551330566, - "intervalMs" : 30.859624966979027, - "previousDrawMs" : 0.24174991995096207 - }, - { - "atSeconds" : 1.2930557085201144, - "currentDrawMs" : 0.27020834386348724, - "intervalMs" : 25.392500218003988, - "previousDrawMs" : 0.24287495762109756 - }, - { - "atSeconds" : 1.5643469998613, - "currentDrawMs" : 4.0951669216156006, - "intervalMs" : 73.542166501283646, - "previousDrawMs" : 59.806666802614927 - }, - { - "atSeconds" : 1.7109711249358952, - "currentDrawMs" : 0.20883325487375259, - "intervalMs" : 27.582958340644836, - "previousDrawMs" : 0.22024987265467644 - }, - { - "atSeconds" : 2.0520284580998123, - "currentDrawMs" : 0.31962525099515915, - "intervalMs" : 35.123374778777361, - "previousDrawMs" : 0.16195839270949364 - }, - { - "atSeconds" : 2.0801404998637736, - "currentDrawMs" : 0.21983357146382332, - "intervalMs" : 27.172041591256857, - "previousDrawMs" : 0.16612512990832329 - }, - { - "atSeconds" : 2.3624465414322913, - "currentDrawMs" : 0.16904203221201897, - "intervalMs" : 28.368083294481039, - "previousDrawMs" : 0.3604586236178875 - }, - { - "atSeconds" : 2.5295559582300484, - "currentDrawMs" : 0.18866686150431633, - "intervalMs" : 29.452375136315823, - "previousDrawMs" : 0.64450036734342575 - }, - { - "atSeconds" : 2.6961935418657959, - "currentDrawMs" : 0.15512481331825256, - "intervalMs" : 27.788917068392038, - "previousDrawMs" : 0.30312500894069672 - }, - { - "atSeconds" : 2.746697333175689, - "currentDrawMs" : 0.1688753254711628, - "intervalMs" : 25.657208170741796, - "previousDrawMs" : 0.19183335825800896 - }, - { - "atSeconds" : 3.028333583381027, - "currentDrawMs" : 0.15616649761795998, - "intervalMs" : 27.204833459109068, - "previousDrawMs" : 0.22962503135204315 - }, - { - "atSeconds" : 3.409057583194226, - "currentDrawMs" : 0.15170825645327568, - "intervalMs" : 25.549208279699087, - "previousDrawMs" : 0.21512527018785477 - }, - { - "atSeconds" : 3.6416789581999183, - "currentDrawMs" : 0.14516664668917656, - "intervalMs" : 25.04695812240243, - "previousDrawMs" : 0.16587506979703903 - }, - { - "atSeconds" : 4.3116195001639426, - "currentDrawMs" : 0.14499993994832039, - "intervalMs" : 27.540708426386118, - "previousDrawMs" : 0.35754172131419182 - }, - { - "atSeconds" : 4.3434893749654293, - "currentDrawMs" : 0.15929155051708221, - "intervalMs" : 26.421958114951849, - "previousDrawMs" : 0.29633333906531334 - }, - { - "atSeconds" : 4.6262941667810082, - "currentDrawMs" : 0.29258336871862411, - "intervalMs" : 26.200791820883751, - "previousDrawMs" : 0.14637503772974014 - }, - { - "atSeconds" : 4.6619038749486208, - "currentDrawMs" : 0.16512488946318626, - "intervalMs" : 27.609166689217091, - "previousDrawMs" : 0.2593332901597023 - }, - { - "atSeconds" : 4.8106183335185051, - "currentDrawMs" : 0.34187501296401024, - "intervalMs" : 25.700750295072794, - "previousDrawMs" : 0.16599986702203751 - }, - { - "atSeconds" : 4.897083958145231, - "currentDrawMs" : 0.18237531185150146, - "intervalMs" : 25.312582962214947, - "previousDrawMs" : 0.27679139748215675 - }, - { - "atSeconds" : 5.2710452498868108, - "currentDrawMs" : 0.17987517639994621, - "intervalMs" : 34.672458190470934, - "previousDrawMs" : 0.28954166918992996 - }, - { - "atSeconds" : 5.3020204585045576, - "currentDrawMs" : 1.8920833244919777, - "intervalMs" : 30.093291774392128, - "previousDrawMs" : 0.21958304569125175 - }, - { - "atSeconds" : 5.4489536248147488, - "currentDrawMs" : 0.23312494158744812, - "intervalMs" : 42.759666685014963, - "previousDrawMs" : 0.25483360514044762 - }, - { - "atSeconds" : 5.5129692917689681, - "currentDrawMs" : 0.24533318355679512, - "intervalMs" : 28.582458384335041, - "previousDrawMs" : 0.56433305144309998 - }, - { - "atSeconds" : 5.6644719168543816, - "currentDrawMs" : 0.21116668358445168, - "intervalMs" : 30.825417023152113, - "previousDrawMs" : 0.39916671812534332 - }, - { - "atSeconds" : 5.7514134584926069, - "currentDrawMs" : 0.48599997535347939, - "intervalMs" : 27.109458576887846, - "previousDrawMs" : 0.29350025579333305 - }, - { - "atSeconds" : 5.9013981251046062, - "currentDrawMs" : 0.40449993684887886, - "intervalMs" : 29.116416815668344, - "previousDrawMs" : 0.36395853385329247 - }, - { - "atSeconds" : 6.0279598333872855, - "currentDrawMs" : 0.25200005620718002, - "intervalMs" : 26.791875250637531, - "previousDrawMs" : 0.53329207003116608 - }, - { - "atSeconds" : 6.0969265415333211, - "currentDrawMs" : 0.26095844805240631, - "intervalMs" : 26.954750064760447, - "previousDrawMs" : 0.26816688477993011 - }, - { - "atSeconds" : 6.2129966667853296, - "currentDrawMs" : 0.18387474119663239, - "intervalMs" : 28.864958323538303, - "previousDrawMs" : 1.4992500655353069 - }, - { - "atSeconds" : 6.2587248333729804, - "currentDrawMs" : 0.36704167723655701, - "intervalMs" : 41.139583569020033, - "previousDrawMs" : 0.28204172849655151 - }, - { - "atSeconds" : 6.3256159583106637, - "currentDrawMs" : 2.7336250059306622, - "intervalMs" : 61.572166625410318, - "previousDrawMs" : 1.3986248522996902 - }, - { - "atSeconds" : 6.3523872499354184, - "currentDrawMs" : 3.5660834982991219, - "intervalMs" : 26.771291624754667, - "previousDrawMs" : 2.7336250059306622 - }, - { - "atSeconds" : 6.4136195834726095, - "currentDrawMs" : 0.17591658979654312, - "intervalMs" : 29.951333533972502, - "previousDrawMs" : 0.25724992156028748 - }, - { - "atSeconds" : 6.7339905416592956, - "currentDrawMs" : 0.27000019326806068, - "intervalMs" : 33.739124890416861, - "previousDrawMs" : 0.19404152408242226 - }, - { - "atSeconds" : 6.8683976666070521, - "currentDrawMs" : 0.30854158103466034, - "intervalMs" : 30.670666601508856, - "previousDrawMs" : 0.19441684707999229 - }, - { - "atSeconds" : 6.9325775001198053, - "currentDrawMs" : 0.49629155546426773, - "intervalMs" : 29.9922083504498, - "previousDrawMs" : 0.28529157862067223 - }, - { - "atSeconds" : 6.9998072083108127, - "currentDrawMs" : 0.25941664353013039, - "intervalMs" : 67.229708191007376, - "previousDrawMs" : 0.49629155546426773 - }, - { - "atSeconds" : 7.0569329583086073, - "currentDrawMs" : 0.5392082966864109, - "intervalMs" : 57.125749997794628, - "previousDrawMs" : 0.25941664353013039 - }, - { - "atSeconds" : 7.2853185832500458, - "currentDrawMs" : 0.23812521249055862, - "intervalMs" : 34.822375047951937, - "previousDrawMs" : 0.32950006425380707 - }, - { - "atSeconds" : 7.3188377497717738, - "currentDrawMs" : 0.21920865401625633, - "intervalMs" : 32.568833325058222, - "previousDrawMs" : 0.16450043767690659 - }, - { - "atSeconds" : 7.4098998331464827, - "currentDrawMs" : 0.27183350175619125, - "intervalMs" : 26.31258312612772, - "previousDrawMs" : 0.1821666955947876 - }, - { - "atSeconds" : 7.5704423747956753, - "currentDrawMs" : 0.58433366939425468, - "intervalMs" : 36.19304159656167, - "previousDrawMs" : 0.21587498486042023 - }, - { - "atSeconds" : 7.6001455001533031, - "currentDrawMs" : 0.60074962675571442, - "intervalMs" : 29.703125357627869, - "previousDrawMs" : 0.58433366939425468 - }, - { - "atSeconds" : 7.657814166508615, - "currentDrawMs" : 0.47358358278870583, - "intervalMs" : 57.668666355311871, - "previousDrawMs" : 0.60074962675571442 - }, - { - "atSeconds" : 7.7805050001479685, - "currentDrawMs" : 0.24516647681593895, - "intervalMs" : 30.213333666324615, - "previousDrawMs" : 0.21720863878726959 - }, - { - "atSeconds" : 7.9032859583385289, - "currentDrawMs" : 0.33716671168804169, - "intervalMs" : 34.589374903589487, - "previousDrawMs" : 0.2543330192565918 - }, - { - "atSeconds" : 8.1446142918430269, - "currentDrawMs" : 0.16983319073915482, - "intervalMs" : 27.559375390410423, - "previousDrawMs" : 0.26225019246339798 - }, - { - "atSeconds" : 8.3100327081046999, - "currentDrawMs" : 0.19175000488758087, - "intervalMs" : 26.437333319336176, - "previousDrawMs" : 0.17750030383467674 - }, - { - "atSeconds" : 8.364564583171159, - "currentDrawMs" : 0.19458355382084846, - "intervalMs" : 25.246916338801384, - "previousDrawMs" : 0.1649162732064724 - }, - { - "atSeconds" : 8.5604942501522601, - "currentDrawMs" : 0.17262483015656471, - "intervalMs" : 26.888875290751457, - "previousDrawMs" : 0.19929185509681702 - }, - { - "atSeconds" : 8.6961341248825192, - "currentDrawMs" : 0.16045849770307541, - "intervalMs" : 29.260999988764524, - "previousDrawMs" : 0.16779173165559769 - }, - { - "atSeconds" : 8.7421492915600538, - "currentDrawMs" : 0.15687523409724236, - "intervalMs" : 25.174208451062441, - "previousDrawMs" : 0.27670850977301598 - }, - { - "atSeconds" : 9.256123083177954, - "currentDrawMs" : 0.68458355963230133, - "intervalMs" : 39.068624842911959, - "previousDrawMs" : 0.16395840793848038 - }, - { - "atSeconds" : 9.4431493747979403, - "currentDrawMs" : 0.4648333415389061, - "intervalMs" : 56.781124789267778, - "previousDrawMs" : 0.2132081426680088 - }, - { - "atSeconds" : 9.4906350001692772, - "currentDrawMs" : 0.65745832398533821, - "intervalMs" : 47.485625371336937, - "previousDrawMs" : 0.4648333415389061 - }, - { - "atSeconds" : 9.5609138747677207, - "currentDrawMs" : 0.22366689518094063, - "intervalMs" : 26.161666493862867, - "previousDrawMs" : 0.28166687116026878 - }, - { - "atSeconds" : 9.6254061250947416, - "currentDrawMs" : 0.18358323723077774, - "intervalMs" : 25.044250302016735, - "previousDrawMs" : 0.3293333575129509 - }, - { - "atSeconds" : 9.7231982084922493, - "currentDrawMs" : 0.21495809778571129, - "intervalMs" : 72.659708559513092, - "previousDrawMs" : 0.15866663306951523 - }, - { - "atSeconds" : 9.8788882084190845, - "currentDrawMs" : 0.19816635176539421, - "intervalMs" : 28.374083340167999, - "previousDrawMs" : 0.29454147443175316 - }, - { - "atSeconds" : 9.9629611251875758, - "currentDrawMs" : 0.77166641131043434, - "intervalMs" : 28.324916958808899, - "previousDrawMs" : 0.27016690000891685 - }, - { - "atSeconds" : 10.020505125168711, - "currentDrawMs" : 0.25012483820319176, - "intervalMs" : 57.54399998113513, - "previousDrawMs" : 0.77166641131043434 - }, - { - "atSeconds" : 10.077614541631192, - "currentDrawMs" : 1.4632497914135456, - "intervalMs" : 57.109416462481022, - "previousDrawMs" : 0.25012483820319176 - }, - { - "atSeconds" : 10.198077749926597, - "currentDrawMs" : 0.56220823898911476, - "intervalMs" : 27.467750012874603, - "previousDrawMs" : 0.53525017574429512 - }, - { - "atSeconds" : 10.302512833382934, - "currentDrawMs" : 0.23195846006274223, - "intervalMs" : 30.668999999761581, - "previousDrawMs" : 0.32904138788580894 - }, - { - "atSeconds" : 10.38018620852381, - "currentDrawMs" : 0.24449964985251427, - "intervalMs" : 29.380833730101585, - "previousDrawMs" : 0.34041702747344971 - }, - { - "atSeconds" : 10.64876962499693, - "currentDrawMs" : 1.917458139359951, - "intervalMs" : 73.654416482895613, - "previousDrawMs" : 0.34279143437743187 - }, - { - "atSeconds" : 10.689420499838889, - "currentDrawMs" : 1.8615000881254673, - "intervalMs" : 40.650874841958284, - "previousDrawMs" : 1.917458139359951 - }, - { - "atSeconds" : 10.809825375210494, - "currentDrawMs" : 2.5671245530247688, - "intervalMs" : 95.951250288635492, - "previousDrawMs" : 0.40366686880588531 - }, - { - "atSeconds" : 10.841733916662633, - "currentDrawMs" : 0.20429166033864021, - "intervalMs" : 27.906250208616257, - "previousDrawMs" : 0.19712513312697411 - }, - { - "atSeconds" : 10.909197291824967, - "currentDrawMs" : 0.22429134696722031, - "intervalMs" : 25.445791892707348, - "previousDrawMs" : 0.29679154977202415 - }, - { - "atSeconds" : 10.966051458381116, - "currentDrawMs" : 0.29270816594362259, - "intervalMs" : 30.576416756957769, - "previousDrawMs" : 0.15849992632865906 - }, - { - "atSeconds" : 11.036102041602135, - "currentDrawMs" : 0.45249983668327332, - "intervalMs" : 27.275208383798599, - "previousDrawMs" : 2.3756669834256172 - }, - { - "atSeconds" : 11.257883916608989, - "currentDrawMs" : 389.14337521418929, - "intervalMs" : 130.0387498922646, - "previousDrawMs" : 0.27895811945199966 - }, - { - "atSeconds" : 11.702675750013441, - "currentDrawMs" : 0.16520824283361435, - "intervalMs" : 444.79183340445161, - "previousDrawMs" : 389.14337521418929 - }, - { - "atSeconds" : 11.832466958090663, - "currentDrawMs" : 0.12066680938005447, - "intervalMs" : 112.41854121908545, - "previousDrawMs" : 2.0668748766183853 - }, - { - "atSeconds" : 12.42131624976173, - "currentDrawMs" : 0.080166850239038467, - "intervalMs" : 588.84929167106748, - "previousDrawMs" : 0.12066680938005447 - }, - { - "atSeconds" : 12.865829458460212, - "currentDrawMs" : 0.72283297777175903, - "intervalMs" : 444.51320869848132, - "previousDrawMs" : 0.080166850239038467 - }, - { - "atSeconds" : 12.939196541439742, - "currentDrawMs" : 0.092958565801382065, - "intervalMs" : 73.367082979530096, - "previousDrawMs" : 0.72283297777175903 - }, - { - "atSeconds" : 12.981409708503634, - "currentDrawMs" : 0.11808332055807114, - "intervalMs" : 42.213167063891888, - "previousDrawMs" : 0.092958565801382065 - }, - { - "atSeconds" : 13.015713833272457, - "currentDrawMs" : 0.11808332055807114, - "intervalMs" : 34.304124768823385, - "previousDrawMs" : 0.11808332055807114 - }, - { - "atSeconds" : 13.081409375183284, - "currentDrawMs" : 0.14362484216690063, - "intervalMs" : 34.342583734542131, - "previousDrawMs" : 0.17479201778769493 - }, - { - "atSeconds" : 13.19690554169938, - "currentDrawMs" : 0.17641671001911163, - "intervalMs" : 25.378458201885223, - "previousDrawMs" : 0.12191664427518845 - }, - { - "atSeconds" : 13.262531625106931, - "currentDrawMs" : 0.16941642388701439, - "intervalMs" : 28.485999908298254, - "previousDrawMs" : 0.18570804968476295 - }, - { - "atSeconds" : 13.432044750079513, - "currentDrawMs" : 0.23587513715028763, - "intervalMs" : 29.914166778326035, - "previousDrawMs" : 0.24762516841292381 - }, - { - "atSeconds" : 13.499584416858852, - "currentDrawMs" : 0.17249956727027893, - "intervalMs" : 32.252333592623472, - "previousDrawMs" : 0.26466650888323784 - }, - { - "atSeconds" : 13.548236791510135, - "currentDrawMs" : 21.830083336681128, - "intervalMs" : 30.776708386838436, - "previousDrawMs" : 0.24100020527839661 - }, - { - "atSeconds" : 13.577083208132535, - "currentDrawMs" : 13.467125128954649, - "intervalMs" : 28.846416622400284, - "previousDrawMs" : 21.830083336681128 - }, - { - "atSeconds" : 13.654752625152469, - "currentDrawMs" : 0.91229146346449852, - "intervalMs" : 77.669417019933462, - "previousDrawMs" : 13.467125128954649 - }, - { - "atSeconds" : 13.689181708265096, - "currentDrawMs" : 4.5067919418215752, - "intervalMs" : 34.429083112627268, - "previousDrawMs" : 0.91229146346449852 - }, - { - "atSeconds" : 14.131113708484918, - "currentDrawMs" : 10.431958362460136, - "intervalMs" : 441.93200021982193, - "previousDrawMs" : 4.5067919418215752 - }, - { - "atSeconds" : 14.170829166658223, - "currentDrawMs" : 0.27925008907914162, - "intervalMs" : 39.715458173304796, - "previousDrawMs" : 10.431958362460136 - }, - { - "atSeconds" : 14.252550208475441, - "currentDrawMs" : 0.14033308252692223, - "intervalMs" : 81.721041817218065, - "previousDrawMs" : 0.27925008907914162 - }, - { - "atSeconds" : 14.281001875177026, - "currentDrawMs" : 0.12433296069502831, - "intervalMs" : 28.451666701585054, - "previousDrawMs" : 0.14033308252692223 - }, - { - "atSeconds" : 14.333090499974787, - "currentDrawMs" : 0.10374980047345161, - "intervalMs" : 52.08862479776144, - "previousDrawMs" : 0.12433296069502831 - }, - { - "atSeconds" : 14.361321333330125, - "currentDrawMs" : 0.16929162666201591, - "intervalMs" : 27.525499928742647, - "previousDrawMs" : 0.39041647687554359 - }, - { - "atSeconds" : 14.395696625113487, - "currentDrawMs" : 0.13970816507935524, - "intervalMs" : 25.790041778236628, - "previousDrawMs" : 0.086749903857707977 - }, - { - "atSeconds" : 14.43063791655004, - "currentDrawMs" : 2.8188335709273815, - "intervalMs" : 34.941291436553001, - "previousDrawMs" : 0.13970816507935524 - }, - { - "atSeconds" : 14.471592708490789, - "currentDrawMs" : 0.15737488865852356, - "intervalMs" : 40.954791940748692, - "previousDrawMs" : 2.8188335709273815 - }, - { - "atSeconds" : 14.525235708337277, - "currentDrawMs" : 0.16054185107350349, - "intervalMs" : 30.301583465188742, - "previousDrawMs" : 0.75741671025753021 - }, - { - "atSeconds" : 14.558456874918193, - "currentDrawMs" : 0.37254160270094872, - "intervalMs" : 31.90558310598135, - "previousDrawMs" : 0.34787505865097046 - }, - { - "atSeconds" : 14.590937458444387, - "currentDrawMs" : 1.4425832778215408, - "intervalMs" : 32.480583526194096, - "previousDrawMs" : 0.37254160270094872 - }, - { - "atSeconds" : 14.622592833358794, - "currentDrawMs" : 0.13162475079298019, - "intervalMs" : 31.65537491440773, - "previousDrawMs" : 1.4425832778215408 - }, - { - "atSeconds" : 14.668349166866392, - "currentDrawMs" : 0.17654150724411011, - "intervalMs" : 44.856875203549862, - "previousDrawMs" : 0.15520816668868065 - }, - { - "atSeconds" : 14.738293208181858, - "currentDrawMs" : 0.19320845603942871, - "intervalMs" : 32.257374841719866, - "previousDrawMs" : 0.21504145115613937 - }, - { - "atSeconds" : 14.76463458314538, - "currentDrawMs" : 0.28095860034227371, - "intervalMs" : 25.508208200335503, - "previousDrawMs" : 0.2099168486893177 - }, - { - "atSeconds" : 14.796448291745037, - "currentDrawMs" : 0.24370802566409111, - "intervalMs" : 25.061916559934616, - "previousDrawMs" : 0.18312502652406693 - }, - { - "atSeconds" : 14.88223333330825, - "currentDrawMs" : 1.7136251553893089, - "intervalMs" : 25.110125076025724, - "previousDrawMs" : 0.27020834386348724 - }, - { - "atSeconds" : 14.913660208228976, - "currentDrawMs" : 0.44533330947160721, - "intervalMs" : 31.426874920725822, - "previousDrawMs" : 1.7136251553893089 - } - ], - "measurementSeconds" : 15, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 61, - "syntheticKeyboardPresses" : 275, - "widthPoints" : 800, - "withBeats" : true, - "withPitchGaps" : true -} \ No newline at end of file diff --git a/docs/validation/staff-beat-cadence-control-800.json b/docs/validation/staff-beat-cadence-control-800.json deleted file mode 100644 index eb4159a..0000000 --- a/docs/validation/staff-beat-cadence-control-800.json +++ /dev/null @@ -1,580 +0,0 @@ -{ - "draw" : { - "maxMs" : 4.5896666124463081, - "meanMs" : 0.37484774814851313, - "medianMs" : 0.24225004017353058, - "p95Ms" : 0.97741652280092239, - "p99Ms" : 2.4482081644237041 - }, - "drawsOver16_67ms" : 0, - "fps" : 51.60218952508162, - "frames" : 332, - "framesWithActiveJoins" : 35, - "framesWithRings" : 314, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 241.82083364576101, - "meanMs" : 19.379022657826226, - "medianMs" : 18.386208452284336, - "p95Ms" : 36.892957985401154, - "p99Ms" : 62.179583124816418 - }, - "intervalsOver25ms" : 91, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - { - "atSeconds" : 0.10934195853769779, - "currentDrawMs" : 0.19054161384701729, - "intervalMs" : 65.765541978180408, - "previousDrawMs" : 0.2326667308807373 - }, - { - "atSeconds" : 0.16508579161018133, - "currentDrawMs" : 0.28112484142184258, - "intervalMs" : 25.633541401475668, - "previousDrawMs" : 0.52849994972348213 - }, - { - "atSeconds" : 0.19189666677266359, - "currentDrawMs" : 0.14575012028217316, - "intervalMs" : 26.810875162482262, - "previousDrawMs" : 0.28112484142184258 - }, - { - "atSeconds" : 0.22837779158726335, - "currentDrawMs" : 0.43991673737764359, - "intervalMs" : 34.078583121299744, - "previousDrawMs" : 0.18812483176589012 - }, - { - "atSeconds" : 0.29812187515199184, - "currentDrawMs" : 0.20391633734107018, - "intervalMs" : 42.110166978091002, - "previousDrawMs" : 0.18891692161560059 - }, - { - "atSeconds" : 0.34840429155156016, - "currentDrawMs" : 0.25716656818985939, - "intervalMs" : 26.089249644428492, - "previousDrawMs" : 0.27562491595745087 - }, - { - "atSeconds" : 0.47087541688233614, - "currentDrawMs" : 0.17187464982271194, - "intervalMs" : 25.266958400607109, - "previousDrawMs" : 0.17154170200228691 - }, - { - "atSeconds" : 0.54038691660389304, - "currentDrawMs" : 0.2238336019217968, - "intervalMs" : 32.533249817788601, - "previousDrawMs" : 0.20295800641179085 - }, - { - "atSeconds" : 0.62497820844873786, - "currentDrawMs" : 0.19341660663485527, - "intervalMs" : 26.92858362570405, - "previousDrawMs" : 0.15220837667584419 - }, - { - "atSeconds" : 0.6746417498216033, - "currentDrawMs" : 0.18683355301618576, - "intervalMs" : 25.770958047360182, - "previousDrawMs" : 0.15520816668868065 - }, - { - "atSeconds" : 0.74538616649806499, - "currentDrawMs" : 0.18404191359877586, - "intervalMs" : 34.86354136839509, - "previousDrawMs" : 0.19699987024068832 - }, - { - "atSeconds" : 0.77441258355975151, - "currentDrawMs" : 0.21529151126742363, - "intervalMs" : 29.026417061686516, - "previousDrawMs" : 0.18404191359877586 - }, - { - "atSeconds" : 0.8210255834273994, - "currentDrawMs" : 0.23429142311215401, - "intervalMs" : 43.117166962474585, - "previousDrawMs" : 0.18316693603992462 - }, - { - "atSeconds" : 0.87106745829805732, - "currentDrawMs" : 0.19566668197512627, - "intervalMs" : 29.50258320197463, - "previousDrawMs" : 0.12962473556399345 - }, - { - "atSeconds" : 1.0209955000318587, - "currentDrawMs" : 0.15558348968625069, - "intervalMs" : 28.55245815590024, - "previousDrawMs" : 0.15233317390084267 - }, - { - "atSeconds" : 1.0704188751988113, - "currentDrawMs" : 0.1554582267999649, - "intervalMs" : 27.737333439290524, - "previousDrawMs" : 0.21691666916012764 - }, - { - "atSeconds" : 1.101693999953568, - "currentDrawMs" : 3.0777500942349434, - "intervalMs" : 26.367624755948782, - "previousDrawMs" : 0.26254169642925262 - }, - { - "atSeconds" : 1.1354957083240151, - "currentDrawMs" : 2.7505415491759777, - "intervalMs" : 28.300791513174772, - "previousDrawMs" : 0.75720809400081635 - }, - { - "atSeconds" : 1.2440534168854356, - "currentDrawMs" : 0.17949985340237617, - "intervalMs" : 25.471416767686605, - "previousDrawMs" : 0.21983310580253601 - }, - { - "atSeconds" : 1.3201279998756945, - "currentDrawMs" : 0.31870836392045021, - "intervalMs" : 28.834083117544651, - "previousDrawMs" : 0.3176247701048851 - }, - { - "atSeconds" : 1.3885177499614656, - "currentDrawMs" : 0.25524990633130074, - "intervalMs" : 25.461750105023384, - "previousDrawMs" : 0.33412501215934753 - }, - { - "atSeconds" : 1.4718020833097398, - "currentDrawMs" : 1.9364999607205391, - "intervalMs" : 30.566416680812836, - "previousDrawMs" : 0.16558356583118439 - }, - { - "atSeconds" : 1.5248524998314679, - "currentDrawMs" : 0.16970839351415634, - "intervalMs" : 32.43412496522069, - "previousDrawMs" : 0.18687499687075615 - }, - { - "atSeconds" : 1.5732365418225527, - "currentDrawMs" : 0.33441651612520218, - "intervalMs" : 27.407083660364151, - "previousDrawMs" : 0.14812499284744263 - }, - { - "atSeconds" : 1.6070365831255913, - "currentDrawMs" : 0.3776252269744873, - "intervalMs" : 27.248083148151636, - "previousDrawMs" : 0.35783322528004646 - }, - { - "atSeconds" : 1.666919291485101, - "currentDrawMs" : 0.17750030383467674, - "intervalMs" : 30.163458082824945, - "previousDrawMs" : 0.23533310741186142 - }, - { - "atSeconds" : 1.7291235416196287, - "currentDrawMs" : 0.20862510427832603, - "intervalMs" : 26.434541679918766, - "previousDrawMs" : 4.5896666124463081 - }, - { - "atSeconds" : 1.9670345415361226, - "currentDrawMs" : 0.28891675174236298, - "intervalMs" : 29.85704131424427, - "previousDrawMs" : 0.18616626039147377 - }, - { - "atSeconds" : 2.1617566249333322, - "currentDrawMs" : 0.2193748950958252, - "intervalMs" : 25.657541584223509, - "previousDrawMs" : 0.22920826449990273 - }, - { - "atSeconds" : 2.2858769167214632, - "currentDrawMs" : 0.94545818865299225, - "intervalMs" : 34.303041640669107, - "previousDrawMs" : 0.20899996161460876 - }, - { - "atSeconds" : 2.3465787498280406, - "currentDrawMs" : 0.38225017488002777, - "intervalMs" : 37.875041365623474, - "previousDrawMs" : 0.21512480452656746 - }, - { - "atSeconds" : 2.550420708488673, - "currentDrawMs" : 0.18987478688359261, - "intervalMs" : 28.038791846483946, - "previousDrawMs" : 0.44008344411849976 - }, - { - "atSeconds" : 2.5917049581184983, - "currentDrawMs" : 0.57962536811828613, - "intervalMs" : 36.892957985401154, - "previousDrawMs" : 0.17108302563428879 - }, - { - "atSeconds" : 2.7248657918535173, - "currentDrawMs" : 0.2076663076877594, - "intervalMs" : 88.959125336259604, - "previousDrawMs" : 0.16479194164276123 - }, - { - "atSeconds" : 2.7602300415746868, - "currentDrawMs" : 0.36312500014901161, - "intervalMs" : 35.364249721169472, - "previousDrawMs" : 0.2076663076877594 - }, - { - "atSeconds" : 2.8698006249032915, - "currentDrawMs" : 0.17154170200228691, - "intervalMs" : 26.44179156050086, - "previousDrawMs" : 1.2147915549576283 - }, - { - "atSeconds" : 2.9206265835091472, - "currentDrawMs" : 0.62895799055695534, - "intervalMs" : 27.311875019222498, - "previousDrawMs" : 0.22066663950681686 - }, - { - "atSeconds" : 2.9744706251658499, - "currentDrawMs" : 0.20949961617588997, - "intervalMs" : 44.33399997651577, - "previousDrawMs" : 0.33574970439076424 - }, - { - "atSeconds" : 3.0347804585471749, - "currentDrawMs" : 0.69316662847995758, - "intervalMs" : 25.815500412136316, - "previousDrawMs" : 0.23979181423783302 - }, - { - "atSeconds" : 3.0900450833141804, - "currentDrawMs" : 0.20670844241976738, - "intervalMs" : 30.338208191096783, - "previousDrawMs" : 0.19279168918728828 - }, - { - "atSeconds" : 3.1905168332159519, - "currentDrawMs" : 0.15387497842311859, - "intervalMs" : 100.47174990177155, - "previousDrawMs" : 0.20670844241976738 - }, - { - "atSeconds" : 3.4323376668617129, - "currentDrawMs" : 0.19858311861753464, - "intervalMs" : 241.82083364576101, - "previousDrawMs" : 0.15387497842311859 - }, - { - "atSeconds" : 3.4958172081969678, - "currentDrawMs" : 0.1777084544301033, - "intervalMs" : 55.350416339933872, - "previousDrawMs" : 0.17841625958681107 - }, - { - "atSeconds" : 3.5407170001417398, - "currentDrawMs" : 0.11645816266536713, - "intervalMs" : 44.899791944772005, - "previousDrawMs" : 0.1777084544301033 - }, - { - "atSeconds" : 3.5879781665280461, - "currentDrawMs" : 0.11537503451108932, - "intervalMs" : 37.934833206236362, - "previousDrawMs" : 0.13204151764512062 - }, - { - "atSeconds" : 3.6342925415374339, - "currentDrawMs" : 1.0168333537876606, - "intervalMs" : 31.798291485756636, - "previousDrawMs" : 0.13845833018422127 - }, - { - "atSeconds" : 3.7856012918055058, - "currentDrawMs" : 0.67495834082365036, - "intervalMs" : 31.257333233952522, - "previousDrawMs" : 1.012999564409256 - }, - { - "atSeconds" : 3.8156289998441935, - "currentDrawMs" : 0.12929178774356842, - "intervalMs" : 26.584249921143055, - "previousDrawMs" : 0.12900028377771378 - }, - { - "atSeconds" : 3.9036059584468603, - "currentDrawMs" : 0.6785830482840538, - "intervalMs" : 28.963708318769932, - "previousDrawMs" : 0.24279160425066948 - }, - { - "atSeconds" : 3.9661211664788425, - "currentDrawMs" : 0.20733335986733437, - "intervalMs" : 29.081499669700861, - "previousDrawMs" : 0.21795835345983505 - }, - { - "atSeconds" : 4.0356638748198748, - "currentDrawMs" : 1.5810420736670494, - "intervalMs" : 25.037124752998352, - "previousDrawMs" : 0.68466644734144211 - }, - { - "atSeconds" : 4.183234749827534, - "currentDrawMs" : 0.42558368295431137, - "intervalMs" : 62.179583124816418, - "previousDrawMs" : 0.76983310282230377 - }, - { - "atSeconds" : 4.3035428752191365, - "currentDrawMs" : 0.21866662427783012, - "intervalMs" : 27.816833462566137, - "previousDrawMs" : 0.24408334866166115 - }, - { - "atSeconds" : 4.4280689582228661, - "currentDrawMs" : 0.27658324688673019, - "intervalMs" : 35.519708413630724, - "previousDrawMs" : 0.25495840236544609 - }, - { - "atSeconds" : 4.5165100418962538, - "currentDrawMs" : 0.18624961376190186, - "intervalMs" : 25.331791955977678, - "previousDrawMs" : 0.25270832702517509 - }, - { - "atSeconds" : 4.543555083218962, - "currentDrawMs" : 0.27637509629130363, - "intervalMs" : 27.04504132270813, - "previousDrawMs" : 0.18624961376190186 - }, - { - "atSeconds" : 4.5745040001347661, - "currentDrawMs" : 0.32291654497385025, - "intervalMs" : 28.677166905254126, - "previousDrawMs" : 0.27383351698517799 - }, - { - "atSeconds" : 4.6012267498299479, - "currentDrawMs" : 0.34737540408968925, - "intervalMs" : 26.722749695181847, - "previousDrawMs" : 0.32291654497385025 - }, - { - "atSeconds" : 4.6514196251519024, - "currentDrawMs" : 0.20854175090789795, - "intervalMs" : 27.026916854083538, - "previousDrawMs" : 0.18008332699537277 - }, - { - "atSeconds" : 4.6857214998453856, - "currentDrawMs" : 0.34295860677957535, - "intervalMs" : 28.438499663025141, - "previousDrawMs" : 0.54258294403553009 - }, - { - "atSeconds" : 4.7636887500993907, - "currentDrawMs" : 0.18095830455422401, - "intervalMs" : 36.189374979585409, - "previousDrawMs" : 2.4482081644237041 - }, - { - "atSeconds" : 4.8132088333368301, - "currentDrawMs" : 0.93445833772420883, - "intervalMs" : 46.883583534508944, - "previousDrawMs" : 0.19841687753796577 - }, - { - "atSeconds" : 4.9011012082919478, - "currentDrawMs" : 0.14933338388800621, - "intervalMs" : 31.390333082526922, - "previousDrawMs" : 0.14470797032117844 - }, - { - "atSeconds" : 4.9689117916859686, - "currentDrawMs" : 0.19920850172638893, - "intervalMs" : 25.437333155423403, - "previousDrawMs" : 0.20270794630050659 - }, - { - "atSeconds" : 5.0188405835069716, - "currentDrawMs" : 0.2826666459441185, - "intervalMs" : 26.350083295255899, - "previousDrawMs" : 0.25395816192030907 - }, - { - "atSeconds" : 5.0710419164970517, - "currentDrawMs" : 0.359333585947752, - "intervalMs" : 25.793791282922029, - "previousDrawMs" : 0.18466636538505554 - }, - { - "atSeconds" : 5.180379833560437, - "currentDrawMs" : 0.25016628205776215, - "intervalMs" : 33.560125157237053, - "previousDrawMs" : 0.37454161792993546 - }, - { - "atSeconds" : 5.2743143332190812, - "currentDrawMs" : 0.20087510347366333, - "intervalMs" : 27.269791346043348, - "previousDrawMs" : 0.28616655617952347 - }, - { - "atSeconds" : 5.3277464583516121, - "currentDrawMs" : 0.42691687121987343, - "intervalMs" : 32.018083147704601, - "previousDrawMs" : 0.70908293128013611 - }, - { - "atSeconds" : 5.3713267082348466, - "currentDrawMs" : 0.26375008746981621, - "intervalMs" : 39.90195831283927, - "previousDrawMs" : 0.17220852896571159 - }, - { - "atSeconds" : 5.4139682501554489, - "currentDrawMs" : 0.27795834466814995, - "intervalMs" : 31.146625056862831, - "previousDrawMs" : 0.38583343848586082 - }, - { - "atSeconds" : 5.4438237915746868, - "currentDrawMs" : 0.41912496089935303, - "intervalMs" : 29.855541419237852, - "previousDrawMs" : 0.27795834466814995 - }, - { - "atSeconds" : 5.4874800001271069, - "currentDrawMs" : 2.1494165994226933, - "intervalMs" : 26.420333422720432, - "previousDrawMs" : 0.19600009545683861 - }, - { - "atSeconds" : 5.5379858748055995, - "currentDrawMs" : 0.28179166838526726, - "intervalMs" : 28.23416655883193, - "previousDrawMs" : 0.30300021171569824 - }, - { - "atSeconds" : 5.5631591668352485, - "currentDrawMs" : 1.2597497552633286, - "intervalMs" : 25.173292029649019, - "previousDrawMs" : 0.28179166838526726 - }, - { - "atSeconds" : 5.5984521666541696, - "currentDrawMs" : 2.6757498271763325, - "intervalMs" : 35.292999818921089, - "previousDrawMs" : 1.2597497552633286 - }, - { - "atSeconds" : 5.6257757917046547, - "currentDrawMs" : 0.38724998012185097, - "intervalMs" : 27.323625050485134, - "previousDrawMs" : 2.6757498271763325 - }, - { - "atSeconds" : 5.6796103748492897, - "currentDrawMs" : 0.36770850419998169, - "intervalMs" : 34.472124651074409, - "previousDrawMs" : 0.33754156902432442 - }, - { - "atSeconds" : 5.7281142501160502, - "currentDrawMs" : 0.44329138472676277, - "intervalMs" : 42.589791584759951, - "previousDrawMs" : 0.1599164679646492 - }, - { - "atSeconds" : 5.7575271250680089, - "currentDrawMs" : 0.3032083623111248, - "intervalMs" : 27.764791622757912, - "previousDrawMs" : 0.22224988788366318 - }, - { - "atSeconds" : 5.8498584581539035, - "currentDrawMs" : 0.50437496975064278, - "intervalMs" : 38.86941634118557, - "previousDrawMs" : 0.16412464901804924 - }, - { - "atSeconds" : 5.9722745418548584, - "currentDrawMs" : 0.29562460258603096, - "intervalMs" : 33.530833665281534, - "previousDrawMs" : 0.3809584304690361 - }, - { - "atSeconds" : 6.0384626667946577, - "currentDrawMs" : 0.24449964985251427, - "intervalMs" : 32.16533362865448, - "previousDrawMs" : 0.17629191279411316 - }, - { - "atSeconds" : 6.1245604166761041, - "currentDrawMs" : 0.22254185751080513, - "intervalMs" : 38.455000147223473, - "previousDrawMs" : 0.26354193687438965 - }, - { - "atSeconds" : 6.1691644168458879, - "currentDrawMs" : 0.37862500175833702, - "intervalMs" : 33.412208314985037, - "previousDrawMs" : 0.30254153534770012 - }, - { - "atSeconds" : 6.2007396249100566, - "currentDrawMs" : 0.18570851534605026, - "intervalMs" : 25.966208428144455, - "previousDrawMs" : 0.25891698896884918 - }, - { - "atSeconds" : 6.2275916668586433, - "currentDrawMs" : 0.59595797210931778, - "intervalMs" : 26.852041948586702, - "previousDrawMs" : 0.18570851534605026 - }, - { - "atSeconds" : 6.2722526248544455, - "currentDrawMs" : 0.27012499049305916, - "intervalMs" : 35.995583049952984, - "previousDrawMs" : 0.34224987030029297 - }, - { - "atSeconds" : 6.3076456668786705, - "currentDrawMs" : 0.60220807790756226, - "intervalMs" : 29.86895851790905, - "previousDrawMs" : 0.31016673892736435 - }, - { - "atSeconds" : 6.3454502918757498, - "currentDrawMs" : 0.87445834651589394, - "intervalMs" : 26.096958667039871, - "previousDrawMs" : 0.26850029826164246 - }, - { - "atSeconds" : 6.4057069164700806, - "currentDrawMs" : 0.3915419802069664, - "intervalMs" : 36.311916541308165, - "previousDrawMs" : 0.27729151770472527 - } - ], - "measurementSeconds" : 6, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 54, - "syntheticKeyboardPresses" : 134, - "widthPoints" : 800, - "withBeats" : true, - "withPitchGaps" : false -} \ No newline at end of file diff --git a/docs/validation/staff-beat-compact-validation.json b/docs/validation/staff-beat-compact-validation.json deleted file mode 100644 index 2c2c23c..0000000 --- a/docs/validation/staff-beat-compact-validation.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "date": "2026-10-26", - "scope": "Compact circular main ripple with three outward small ripples", - "mainRadiusRangePoints": [ - 8, - 36 - ], - "satelliteRadiusRangePoints": [ - 2.5, - 12 - ], - "maximumGroupDiameterPoints": 108, - "groupLifetimeMs": 670, - "pulseLimit": 4, - "ringLimit": 16, - "tests": { - "passed": 28, - "failed": 0 - }, - "performance": "staff-beat-performance.json", - "nativeCadence": "staff-beat-cadence-800.json", - "controlCadence": "staff-beat-cadence-control-800.json", - "demo": "staff-beat-demo.json", - "measurementLimits": "Both cadence runs had long frames while other desktop work was active. CPU-only costs exclude scheduling, WindowServer and GPU work.", - "binarySHA256": "94f0594844239bc3b0a91c138dd56ed75ac332d4b60c8aebaed65c3e0d816a84", - "restarted": true, - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffBeatRings.swift": "92a46d3728194683c74bebe49f45658e6107bdcdd6001cdd6cbd0b4cdfa00ece", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "4f360cdebc1879f3881c199c8dc33364d4eddffa4698c93e5167fb8fa25ba915" - } -} diff --git a/docs/validation/staff-beat-demo.json b/docs/validation/staff-beat-demo.json deleted file mode 100644 index 5788aab..0000000 --- a/docs/validation/staff-beat-demo.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "animationRateIsPresentationBenchmark": false, - "background": "plain paper", - "beats": 20, - "durationSeconds": 10, - "fps": 25, - "frames": 250, - "framesWithBeatRings": 250, - "framesWithStars": 242, - "height": 240, - "keyboardBursts": 48, - "keyboardPresses": 39, - "keyboardRows": [ - 0, - 1, - 2, - 3, - 4 - ], - "loop": "infinite", - "mixtureComponents": [ - "harmonic lead", - "bass", - "percussive noise" - ], - "notationHeightRange": 27.999999204642776, - "noteCenterX": 640, - "notes": [ - "A4", - "B4", - "C5", - "D4", - "D5", - "E4", - "F♯4", - "G4" - ], - "onsets": 20, - "peakStars": 40, - "scaleRange": [ - 0.8001295939857936, - 1.4991612234125296 - ], - "source": "generated lead, bass and percussion mixture plus invented physical key events; production audio analyzers, keyboard channel, star simulation and retained native layers; no capture or event injection", - "staticPreviewFrame": 110, - "swayDegrees": [ - -19.83672386349515, - 19.43535777762688 - ], - "warmupSeconds": 10, - "width": 1200 -} diff --git a/docs/validation/staff-beat-performance.json b/docs/validation/staff-beat-performance.json deleted file mode 100644 index fe01233..0000000 --- a/docs/validation/staff-beat-performance.json +++ /dev/null @@ -1,327 +0,0 @@ -{ - "beatStress" : "180 BPM during typing; four simultaneous artificial pulses every 500ms in capacity case to saturate all sixteen main\/small ring slots", - "burstLimit" : 16, - "method" : "600 measured synthetic frames after 9s warmup per size\/pattern; production keyboard channel, native staff session and retained layer transactions; CLOCK_THREAD_CPUTIME_ID records main-thread CPU, systemUptime separately includes scheduling\/commit waits; excludes WindowServer\/GPU execution and presentation", - "p95BudgetMs" : 2, - "passed" : true, - "pitchGapStress" : "In gap mode, four unvoiced frames in every ten create overlapping joins while new notes continue", - "ringLimit" : 16, - "scenarios" : [ - { - "animationUpdate" : { - "maxMs" : 0.64020836725831032, - "meanMs" : 0.024711669733126961, - "medianMs" : 0.01583341509103775, - "p95Ms" : 0.058250036090612411, - "p99Ms" : 0.10816659778356552 - }, - "combined" : { - "maxMs" : 16.294958535581827, - "meanMs" : 0.76453778815145301, - "medianMs" : 0.2277083694934845, - "p95Ms" : 4.5405002310872078, - "p99Ms" : 8.9207086712121964 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 16.266625374555588, - "meanMs" : 0.73982611841832602, - "medianMs" : 0.20512519404292107, - "p95Ms" : 4.5176250860095024, - "p99Ms" : 8.8732503354549408 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakActiveJoins" : 2, - "peakRings" : 8, - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.518042, - "meanMs" : 0.19646750500000004, - "medianMs" : 0.18112500000000001, - "p95Ms" : 0.33312500000000006, - "p99Ms" : 0.43049999999999999 - }, - "threadCPUSubmission" : { - "maxMs" : 0.448125, - "meanMs" : 0.17541874000000024, - "medianMs" : 0.15929200000000002, - "p95Ms" : 0.29375000000000001, - "p99Ms" : 0.370666 - }, - "threadCPUUpdate" : { - "maxMs" : 0.106625, - "meanMs" : 0.021048765000000011, - "medianMs" : 0.015125, - "p95Ms" : 0.047334000000000001, - "p99Ms" : 0.074666999999999997 - }, - "wallP95Under2ms" : false, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 0.39074989035725594, - "meanMs" : 0.029533337025592722, - "medianMs" : 0.022708438336849213, - "p95Ms" : 0.056708231568336487, - "p99Ms" : 0.1399582251906395 - }, - "combined" : { - "maxMs" : 14.018958434462547, - "meanMs" : 0.42226646250734728, - "medianMs" : 0.17174985259771347, - "p95Ms" : 0.93449978157877922, - "p99Ms" : 6.0875415802001953 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 13.988708145916462, - "meanMs" : 0.39273312548175454, - "medianMs" : 0.14366675168275833, - "p95Ms" : 0.81333331763744354, - "p99Ms" : 6.0645416378974915 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakActiveJoins" : 2, - "peakRings" : 16, - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.38774999999999998, - "meanMs" : 0.14938834500000001, - "medianMs" : 0.137625, - "p95Ms" : 0.244084, - "p99Ms" : 0.31270799999999999 - }, - "threadCPUSubmission" : { - "maxMs" : 0.33037499999999997, - "meanMs" : 0.12338334499999995, - "medianMs" : 0.111708, - "p95Ms" : 0.201542, - "p99Ms" : 0.27575 - }, - "threadCPUUpdate" : { - "maxMs" : 0.099250000000000005, - "meanMs" : 0.026005000000000014, - "medianMs" : 0.022041999999999999, - "p95Ms" : 0.046290999999999999, - "p99Ms" : 0.061333000000000006 - }, - "wallP95Under2ms" : true, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 0.10470813140273094, - "meanMs" : 0.0098472166185577716, - "medianMs" : 0.0081248581409454346, - "p95Ms" : 0.021083280444145203, - "p99Ms" : 0.044333282858133316 - }, - "combined" : { - "maxMs" : 13.318249955773354, - "meanMs" : 0.27931186991433304, - "medianMs" : 0.12208335101604462, - "p95Ms" : 0.26108324527740479, - "p99Ms" : 5.371458362787962 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 13.309291563928127, - "meanMs" : 0.26946465329577524, - "medianMs" : 0.11366652324795723, - "p95Ms" : 0.24808337911963463, - "p99Ms" : 5.3639998659491539 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakActiveJoins" : 2, - "peakRings" : 8, - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.29795799999999995, - "meanMs" : 0.11488634833333333, - "medianMs" : 0.10408299999999999, - "p95Ms" : 0.17533399999999999, - "p99Ms" : 0.24687500000000001 - }, - "threadCPUSubmission" : { - "maxMs" : 0.27775, - "meanMs" : 0.10556667333333339, - "medianMs" : 0.095709000000000002, - "p95Ms" : 0.15987499999999999, - "p99Ms" : 0.22237499999999999 - }, - "threadCPUUpdate" : { - "maxMs" : 0.093332999999999999, - "meanMs" : 0.0093196750000000012, - "medianMs" : 0.0078329999999999997, - "p95Ms" : 0.020166, - "p99Ms" : 0.041291000000000001 - }, - "wallP95Under2ms" : true, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 0.16333302482962608, - "meanMs" : 0.025693046239515144, - "medianMs" : 0.020166859030723572, - "p95Ms" : 0.054124742746353149, - "p99Ms" : 0.082583166658878326 - }, - "combined" : { - "maxMs" : 17.492250073701143, - "meanMs" : 0.55963347355524706, - "medianMs" : 0.15166634693741798, - "p95Ms" : 3.1266668811440468, - "p99Ms" : 10.296875145286322 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 17.465208657085896, - "meanMs" : 0.53394042731573188, - "medianMs" : 0.12791622430086136, - "p95Ms" : 3.1061670742928982, - "p99Ms" : 10.255083441734314 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakActiveJoins" : 2, - "peakRings" : 16, - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.41341700000000003, - "meanMs" : 0.13767053166666665, - "medianMs" : 0.121875, - "p95Ms" : 0.24783299999999997, - "p99Ms" : 0.313666 - }, - "threadCPUSubmission" : { - "maxMs" : 0.36066700000000002, - "meanMs" : 0.11370931000000002, - "medianMs" : 0.099957999999999991, - "p95Ms" : 0.20183300000000001, - "p99Ms" : 0.263959 - }, - "threadCPUUpdate" : { - "maxMs" : 0.086874999999999994, - "meanMs" : 0.023961221666666681, - "medianMs" : 0.019667, - "p95Ms" : 0.047541, - "p99Ms" : 0.064750000000000002 - }, - "wallP95Under2ms" : false, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 0.157166738063097, - "meanMs" : 0.014562439173460007, - "medianMs" : 0.0096252188086509705, - "p95Ms" : 0.036874786019325256, - "p99Ms" : 0.072875060141086578 - }, - "combined" : { - "maxMs" : 11.954333167523146, - "meanMs" : 0.47704854669670266, - "medianMs" : 0.15562493354082108, - "p95Ms" : 3.2681669108569622, - "p99Ms" : 6.0302084311842918 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 11.931541841477156, - "meanMs" : 0.46248610752324265, - "medianMs" : 0.14045834541320801, - "p95Ms" : 3.2335417345166206, - "p99Ms" : 5.9826248325407505 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakActiveJoins" : 2, - "peakRings" : 8, - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.358458, - "meanMs" : 0.13476957500000006, - "medianMs" : 0.121375, - "p95Ms" : 0.22541599999999998, - "p99Ms" : 0.29191699999999998 - }, - "threadCPUSubmission" : { - "maxMs" : 0.34458299999999997, - "meanMs" : 0.12196429499999992, - "medianMs" : 0.109416, - "p95Ms" : 0.19629200000000002, - "p99Ms" : 0.26308300000000001 - }, - "threadCPUUpdate" : { - "maxMs" : 0.11270899999999999, - "meanMs" : 0.012805279999999995, - "medianMs" : 0.0092919999999999999, - "p95Ms" : 0.029124999999999998, - "p99Ms" : 0.049042000000000002 - }, - "wallP95Under2ms" : false, - "widthPoints" : 1600 - }, - { - "animationUpdate" : { - "maxMs" : 1.4281249605119228, - "meanMs" : 0.030688044304649036, - "medianMs" : 0.018541701138019562, - "p95Ms" : 0.06374996155500412, - "p99Ms" : 0.12729177251458168 - }, - "combined" : { - "maxMs" : 45.322416815906763, - "meanMs" : 0.7537065943082174, - "medianMs" : 0.13879174366593361, - "p95Ms" : 4.2292503640055656, - "p99Ms" : 10.425916872918606 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 45.285708270967007, - "meanMs" : 0.72301855000356841, - "medianMs" : 0.11670822277665138, - "p95Ms" : 4.1251666843891144, - "p99Ms" : 10.375958401709795 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakActiveJoins" : 2, - "peakRings" : 16, - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.551875, - "meanMs" : 0.13327472333333332, - "medianMs" : 0.1115, - "p95Ms" : 0.26145800000000002, - "p99Ms" : 0.33474999999999999 - }, - "threadCPUSubmission" : { - "maxMs" : 0.42920799999999998, - "meanMs" : 0.10914083833333327, - "medianMs" : 0.091415999999999997, - "p95Ms" : 0.220916, - "p99Ms" : 0.28487500000000004 - }, - "threadCPUUpdate" : { - "maxMs" : 0.199125, - "meanMs" : 0.024133884999999994, - "medianMs" : 0.018166999999999999, - "p95Ms" : 0.053582999999999999, - "p99Ms" : 0.069165999999999991 - }, - "wallP95Under2ms" : false, - "widthPoints" : 1600 - } - ], - "starLimit" : 112, - "typingPressesPerSecond" : 20, - "withBeats" : true, - "withPitchGaps" : true -} \ No newline at end of file diff --git a/docs/validation/staff-beat-tests.json b/docs/validation/staff-beat-tests.json deleted file mode 100644 index 00acb6c..0000000 --- a/docs/validation/staff-beat-tests.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "checks" : [ - { - "name" : "A beat softly reveals two main arcs and starts a small ripple without replaying repeated snapshots", - "passed" : true - }, - { - "name" : "The main ripple keeps its random center while its smaller companion moves outwards", - "passed" : true - }, - { - "name" : "The smaller main wave remains clearly visible with a maximum 64-point diameter", - "passed" : true - }, - { - "name" : "Two main arcs carry three distinct smaller arcs with a compact outward spread", - "passed" : true - }, - { - "name" : "The delayed inner arc softly finishes after the outer arc", - "passed" : true - }, - { - "name" : "Expiration requests a final redraw and a settled ring layer stops invalidation", - "passed" : true - }, - { - "name" : "Stale or low-confidence beat events never create rings", - "passed" : true - }, - { - "name" : "A fresh pulse after a long frame is shown at its actual age", - "passed" : true - }, - { - "name" : "A pathological backlog retains only four recent beat pulses", - "passed" : true - }, - { - "name" : "Four simultaneous main-and-small bursts fit twenty reusable arc slots", - "passed" : true - }, - { - "name" : "A long frame expires all old rings without replaying them", - "passed" : true - }, - { - "name" : "New beats scatter across the staff with safe margins and separated successive centers", - "passed" : true - }, - { - "name" : "A percussion-only frame with no vocal pitch still displays a ring", - "passed" : true - }, - { - "name" : "Beat rings, stars, pitch movement, sway and volume scaling run together without changing musical positions", - "passed" : true - }, - { - "name" : "Silence clears rings while existing keyboard stars keep fading", - "passed" : true - }, - { - "name" : "Stale transport snapshots clear rings and cannot replay their beat", - "passed" : true - }, - { - "name" : "Format changes discard prior rings and queued events", - "passed" : true - }, - { - "name" : "A new audio generation clears the previous pulse clock", - "passed" : true - }, - { - "name" : "Hiding or switching a module removes cached rings before the first redraw", - "passed" : true - }, - { - "name" : "Settled silent input stops requesting animation updates", - "passed" : true - }, - { - "name" : "Twenty cached tapered arcs sit above stars and below note, marker and names", - "passed" : true - }, - { - "name" : "Main wave consists of three open arcs with softened tips", - "passed" : true - }, - { - "name" : "Rings stay at their own centers through pitch changes and scale uniformly at 600\/800\/1600 points", - "passed" : true - }, - { - "name" : "Expanding an arc preserves thin logical strokes and halo widths", - "passed" : true - }, - { - "name" : "Animation retains the same twenty layer objects and cached arc path", - "passed" : true - }, - { - "name" : "Expired ring layers are hidden and remain reusable", - "passed" : true - }, - { - "name" : "All five global pen colors reach the tapered native arcs", - "passed" : true - }, - { - "name" : "Rings are visible on both backgrounds at all sizes and preserve the solid head", - "passed" : true - }, - { - "name" : "Native layers and export drawing softly fade rings at canvas edges at all sizes", - "passed" : true - } - ], - "failed" : 0, - "passed" : 29 -} \ No newline at end of file diff --git a/docs/validation/staff-beat-validation.json b/docs/validation/staff-beat-validation.json deleted file mode 100644 index 3fbbd49..0000000 --- a/docs/validation/staff-beat-validation.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "scope": "Beat tracking and rings; existing uncommitted pitch and trajectory fixes are preserved", - "tests": { - "beat-tracker-tests": { - "passed": 24, - "failed": 0 - }, - "staff-beat-tests": { - "passed": 20, - "failed": 0 - }, - "contract-tests": { - "passed": 20, - "failed": 0 - }, - "staff-note-tests": { - "passed": 89, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 40, - "failed": 0 - }, - "staff-keyboard-tests": { - "passed": 36, - "failed": 0 - }, - "staff-keyboard-platform-tests": { - "passed": 20, - "failed": 0 - }, - "audio-feature-tests": { - "passed": 28, - "failed": 0 - }, - "audio-pipeline-tests": { - "passed": 13, - "failed": 0 - } - }, - "totalPassed": 290, - "totalFailed": 0, - "appBinarySHA256": "d4d44b3123407a3052b1786121e6bed0f497e7552290a80d4aec9a27d16c02ea", - "performance": "staff-beat-performance.json", - "nativeCadence": "staff-beat-cadence-800.json", - "songSmoke": "beat-song-smoke.json", - "liveVerification": { - "installedApplication": "/Applications/Miucs.app", - "restarted": true, - "runningPID": 66903, - "capture": "ScreenCaptureKit 48000Hz stereo with Rapidly and CREPE still active", - "observed": "Causal beat ring visibly surrounding the moving note head during playback of an existing uninterrupted Ani_2_06 mixture; all playback initiated for this check has stopped", - "screenshotCommitted": false - }, - "visualRedesign": { - "date": "2026-10-25", - "scope": "Larger delayed dual rings, constant-width pencil strokes and soft canvas edges; beat detection unchanged", - "pulseLifetimeMs": 780, - "ringLifetimeMs": 680, - "echoDelayMs": 100, - "primaryRadiusRangePoints": [ - 14, - 84 - ], - "pulseLimit": 4, - "ringLimit": 8, - "tests": { - "staff-beat-tests": { - "passed": 25, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 40, - "failed": 0 - } - }, - "appBinarySHA256": "ab5c87656b8ced2ebc77c35ff46dfbc9d05a7066cf6ec9465f7cebbf271ffa0d", - "performance": "staff-beat-performance.json", - "nativeCadence": "staff-beat-cadence-800.json", - "demo": "staff-beat-demo.json", - "restarted": true, - "previousValidation": "Top-level 290-check record describes the original beat implementation, before this visual redesign", - "liveVerification": { - "installedApplication": "/Applications/Miucs.app", - "capture": "ScreenCaptureKit 48000Hz stereo; existing Rapidly and CREPE active", - "observed": "Large concentric rings visibly surround the G4 note while the trace moves, using bounded 120 BPM generated PCM playback", - "syntheticPlaybackStopped": true, - "screenshotCommitted": false - } - }, - "randomPlacementRedesign": { - "date": "2026-10-25", - "scope": "Each beat selects one random center; both expanding waves keep that center independently of the note. Beat detection is unchanged.", - "horizontalRangePoints": [ - 112, - 712 - ], - "verticalRangePoints": [ - 36, - 124 - ], - "minimumSuccessiveHorizontalDistancePoints": 120, - "primaryRadiusRangePoints": [ - 14, - 84 - ], - "pulseLifetimeMs": 780, - "pulseLimit": 4, - "ringLimit": 8, - "tests": { - "staff-beat-tests": { - "passed": 27, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 40, - "failed": 0 - } - }, - "appBinarySHA256": "e946dac68168a70f92f25a8c488032b1c4139474256a83a5d9e941ca921a44c0", - "restarted": true, - "performance": "staff-beat-performance.json", - "nativeCadence": "staff-beat-cadence-800.json", - "demo": "staff-beat-demo.json", - "liveVerification": { - "capture": "ScreenCaptureKit 48000Hz stereo with existing Rapidly and CREPE", - "observed": "Independent ring center above the G4 note during bounded generated 120 BPM playback; distribution and fixed-origin behavior checked in core/native tests", - "syntheticPlaybackStopped": true, - "screenshotCommitted": false - }, - "previousValidation": "Top-level and visualRedesign entries describe the two earlier beat revisions; linked generated reports now describe this revision." - } -} diff --git a/docs/validation/staff-contour-validation.json b/docs/validation/staff-contour-validation.json deleted file mode 100644 index e9a59f4..0000000 --- a/docs/validation/staff-contour-validation.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "date": "2026-10-23", - "scope": "Restore G2-C6 pitch following; keep an audible phrase connected through filtered pitch and remove extra ledger strokes", - "behavior": { - "followingRange": "G2-C6; rejected candidates never clamp to a range edge", - "jumpConfirmationMinimumMilliseconds": [ - 100, - 125, - 150 - ], - "displayRegister": "Whole-history adaptive octave display retained; true sounding pitch names remain unchanged", - "contour": "Filtering no longer splits an audible phrase after 350ms. Reliable points remain connected while audio continues; silence, disconnection or expired history ends the segment.", - "movingHead": "The last 10 logical points blend smoothly toward the animated head and the visual path ends at its center; accepted pitch data is unchanged.", - "limits": "Only accepted pitches enter history. The 8-second/512-point limit remains; the visual tail disappears when all reliable history expires.", - "extraHorizontalLines": "Removed ledger paths and layers from native and offline renderers; the five main staff lines, note, sharp signs, true pitch label and keyboard stars remain." - }, - "tests": { - "staff-note-tests": { - "passed": 75, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 35, - "failed": 0 - }, - "staff-keyboard-tests": { - "passed": 36, - "failed": 0 - }, - "staff-keyboard-platform-tests": { - "passed": 20, - "failed": 0 - } - }, - "totalPassed": 166, - "totalFailed": 0, - "regressions": [ - "Mixed missing, out-of-range and low-confidence inputs including 1.2-second gaps preserve one audible phrase without invented samples", - "Path stays attached to the moving note and remains one native subpath across a gap longer than 350ms", - "True silence still separates subpaths; eight seconds without accepted pitch expires the visual tail", - "Low and high notes temporarily outside the five lines have no ledger strokes in either renderer at 600/800/1600 points", - "All five main staff lines remain visible", - "Keyboard star pool, colors, scaling, note occlusion, audio concurrency and idle rendering regressions pass" - ], - "visualInspection": [ - "Synthetic native low C3 transient: no stacked short horizontal lines beside the note", - "Synthetic 800-point light phrase: connected contour reaches the note", - "Restarted installed application window observed in idle state" - ], - "commands": [ - "zsh scripts/test-staff-note.sh", - "zsh scripts/test-staff-note-platform.sh", - "zsh scripts/test-staff-keyboard.sh", - "zsh build.sh", - "git diff --check" - ], - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffPitch.swift": "8964e76630b44271a28df8c6e2b42da2185263c2d5acbd747b0d00ed18e65578", - "modules/visualizations/staff-note/core/StaffPitchTracker.swift": "3fde5ae30b8d0c9d083fc63b0bd0f0044aab68aa6ef76f6b3ce31adbb259ffe2", - "modules/visualizations/staff-note/core/StaffDisplayRegister.swift": "01c5f1146f59c383b74707abe2b168466832351654189f4c3ce5965c23d033b7", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "3d43e9842f5bb97c0cf20b9e04dfc3911b9e4578557035735572e40512cfec70", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "ac20f77b54cebc841552c360a0fd5283042fa028843dd854eb2e954c8fe56b6d", - "tests/visualizations/StaffNoteTests.swift": "a36c1729bc26ff89100690c3c3b372bfe3a5fa76c4f7749602f63d4e157f6ecb", - "tests/platforms/macos/StaffNotePlatformTests.swift": "892da7965d1b8a40ba1eabe21cea59285b31ff30d5a15afb2dd7aa5a49258638", - "tests/platforms/macos/StaffKeyboardPlatformTests.swift": "a88a0bcf5ef5632650d483af16789308d79a39b73563015eeb042e4a4877f281" - }, - "delivery": { - "installedApp": "/Applications/Miucs.app", - "executableSHA256": "f07f000d238df38ff30a596946cce371f38009cc532cc82197fd3e4693014b18", - "installedMatchesBuild": true, - "signaturePassed": true, - "previousAppBackup": "build/Miucs-before-contour-update-20261023-233159.app", - "restartStatus": "running", - "runningInstalledPID": 35808, - "observedWindowTitle": "Miucs · 五线谱音符" - }, - "listeningTrial": "Pending user trial with the original music; synthetic tests and an idle live screenshot do not constitute listening acceptance." -} diff --git a/docs/validation/staff-demo.json b/docs/validation/staff-demo.json deleted file mode 100644 index ab6f5b0..0000000 --- a/docs/validation/staff-demo.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "date": "2026-10-26", - "command": "zsh scripts/render-demo.sh --staff", - "baseRevision": "376162084339753ca57371c07d08cd2a05369730", - "asset": "docs/assets/miucs-staff-note.gif", - "assetSHA256": "6ad891071d98bd46f1f531836fd469bc519b1ae4a10928d27f0c7133e07547b4", - "staticPreview": { - "asset": "docs/assets/miucs-staff-note.png", - "frame": 110, - "size": [ - 1200, - 240 - ], - "exactlyMatchesGIFFrame": true, - "source": "Selected production-rendered animation frame, exported after GIF color conversion to preserve identical pixels" - }, - "staticPreviewSHA256": "a9042e2916e8f0d262dd7e834774de081e84c2b9a33df8f1d8b6328fa86a371e", - "render": { - "animationRateIsPresentationBenchmark": false, - "background": "plain paper", - "beats": 20, - "durationSeconds": 10, - "fps": 25, - "frames": 250, - "framesWithBeatRings": 250, - "framesWithStars": 242, - "height": 240, - "keyboardBursts": 48, - "keyboardPresses": 39, - "keyboardRows": [ - 0, - 1, - 2, - 3, - 4 - ], - "loop": "infinite", - "mixtureComponents": [ - "harmonic lead", - "bass", - "percussive noise" - ], - "notationHeightRange": 27.999999204642776, - "noteCenterX": 640, - "notes": [ - "A4", - "B4", - "C5", - "D4", - "D5", - "E4", - "F♯4", - "G4" - ], - "onsets": 20, - "peakStars": 40, - "scaleRange": [ - 0.8001295939857936, - 1.4991612234125296 - ], - "source": "generated lead, bass and percussion mixture plus invented physical key events; production audio analyzers, keyboard channel, star simulation and retained native layers; no capture or event injection", - "staticPreviewFrame": 110, - "swayDegrees": [ - -19.83672386349515, - 19.43535777762688 - ], - "warmupSeconds": 10, - "width": 1200 - }, - "encoding": { - "frames": 250, - "size": [ - 1200, - 240 - ], - "durationSeconds": 10, - "fps": 25, - "bytes": 1867589, - "loop": "infinite", - "metadata": "format and animation fields only; inherited metadata removed", - "allFramesChange": true, - "allFramesMatchQuantizedSource": true, - "source": "offline generated PCM and application renderer" - }, - "visualReview": { - "frames": [ - 0, - 62, - 125, - 187, - 249 - ], - "staticFrame": 110, - "result": "Current native mixture-driven staff, melody trail, pivot sway and scaling, keyboard starbursts and random beat arcs reviewed. Native output uses generated PCM and invented keys; no live capture." - }, - "sourceSHA256": { - "tools/macos/StaffNoteDemo.swift": "97f918153c755cc9e7cd40353f7389f071eff631bcfa62093b21d81287e4ecea", - "tools/macos/SyntheticKeyboardInput.swift": "de193fa236b70265c969e90a9f6477b221dd3e3526fdf3046790f837b5bc4c0e", - "tools/macos/RenderDemo.swift": "0b808f52fcd15d0a24b01cc0fe948dbde81d1b1cbc2d3c316bfa5fbd2d607a4b", - "scripts/render-demo.sh": "69768d7dc212d4964940c1b5c1790f6319771dd56b4309f0854e7875285bd4d6", - "scripts/prepare-demo.py": "a939339b726adf5cd889b2d9042ec34617e3d22820b37e1b811cb8a0a00846d2", - "modules/audio/core/AudioAnalysisPipeline.swift": "39364bbad9d38b44763620e2e9d14d83b3bb88dfe81257bc0711e86eb002e2cd", - "modules/audio/core/MelodyAnalyzer.swift": "b0ab87ca59ea900e992a1c303277921bc576e6706f85fb06e87175ea170f48e1", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "e74c21712b148102aebc4dcf055d460cda7ef1a4d95ad2488edc1a3845c674fd", - "modules/visualizations/staff-note/core/StaffKeyboardStars.swift": "e4a20201621a7cb0192e41b0eae4f9f5cdb80a9e2d2031ea6f20e7dcf9f397aa", - "modules/visualizations/staff-note/core/StaffBeatRings.swift": "e2576b85823da1b232d49306fc3fc4bd7c9b5bb4d10c9a5530139de26bb6eb17", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "dd4a9bf38c3ebea6fc474bca7eb9456debec1904501abc888ab3e8b57c4a2bc2" - } -} diff --git a/docs/validation/staff-fixed-pitch-validation.json b/docs/validation/staff-fixed-pitch-validation.json deleted file mode 100644 index ebe8c9c..0000000 --- a/docs/validation/staff-fixed-pitch-validation.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "date": "2026-10-23", - "scope": "Remove adaptive display-octave recentering so each real pitch always has the same absolute staff position", - "fixedStaff": { - "clef": "treble", - "lineY": [ - 58, - 66, - 74, - 82, - 90 - ], - "exampleNoteY": { - "C4": 98, - "G4": 82, - "B4": 74 - }, - "automaticOctaveTranslation": false, - "dynamicOctaveClefLabels": false, - "transition": "Only accepted pitch changes move the target; the existing critically damped transition remains", - "history": "Previously accepted pitches keep their absolute notation height; only the live 10-point tail blends toward the moving head" - }, - "preservedBehavior": [ - "G2-C6 following range", - "100/125/150ms jump confirmation", - "Audible phrase continuity through rejected pitch results", - "No extra ledger strokes", - "Concurrent keyboard stars and audio-driven sway/scale" - ], - "tests": { - "staff-note-tests": { - "passed": 72, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 37, - "failed": 0 - }, - "staff-keyboard-tests": { - "passed": 36, - "failed": 0 - }, - "staff-keyboard-platform-tests": { - "passed": 20, - "failed": 0 - } - }, - "totalPassed": 165, - "totalFailed": 0, - "specificRegressions": [ - "Every accepted G2-C6 pitch settles at its canonical staff height", - "C4 returns to the same height after low and high preceding phrases and input reset", - "Holding C4 for four further seconds never recenters it", - "Later low phrases preserve old C4 history and the full octave distance", - "Native note and marker plus reference rendering agree with fixed low/high staff positions", - "Native path retains the original C4 height after a later low phrase", - "Existing trace continuity, ledger-removal and keyboard concurrency checks pass" - ], - "commands": [ - "zsh scripts/test-staff-note.sh", - "zsh scripts/test-staff-note-platform.sh", - "zsh scripts/test-staff-keyboard.sh", - "zsh build.sh", - "git diff --check" - ], - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffPitch.swift": "8964e76630b44271a28df8c6e2b42da2185263c2d5acbd747b0d00ed18e65578", - "modules/visualizations/staff-note/core/StaffPitchTracker.swift": "3fde5ae30b8d0c9d083fc63b0bd0f0044aab68aa6ef76f6b3ce31adbb259ffe2", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "7d437bcdd7d082b7eb94fd0d4f5c8bcaa8b5352fe6bdf05e572a0993b1e8a258", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "0e635974a241321667485661cc6646e6bbbba749988dc1aeba7f138bcb588f8b", - "tests/visualizations/StaffNoteTests.swift": "7c4ca2d48da9b592ef121469b63c60c0cf42931ec865d236800904a339ce63ae", - "tests/platforms/macos/StaffNotePlatformTests.swift": "82ccebae42981edf3f99a3aa86d6ec5e136979c0480448ca27393da4ad6143c3", - "tests/platforms/macos/StaffKeyboardPlatformTests.swift": "9ef78bafd045173151625a414df30220fa55749315f88f4c4cf5db0d8aad9528" - }, - "delivery": { - "installedApp": "/Applications/Miucs.app", - "executableSHA256": "7a6470acf1d51a23b092ebaaf8c7517b06987b8f781ed321d64c04ddf4cdb254", - "installedMatchesBuild": true, - "signaturePassed": true, - "previousAppBackup": "build/Miucs-before-fixed-pitch-20261023-235603.app", - "restartStatus": "running", - "runningInstalledPID": 63972, - "observedWindowTitle": "Miucs · 五线谱音符" - }, - "listeningTrial": "Automated simulation/native rendering passed and installed application restarted; user listening acceptance has not yet been claimed." -} diff --git a/docs/validation/staff-join-cadence-800.json b/docs/validation/staff-join-cadence-800.json deleted file mode 100644 index e8f19f4..0000000 --- a/docs/validation/staff-join-cadence-800.json +++ /dev/null @@ -1,214 +0,0 @@ -{ - "draw" : { - "maxMs" : 30.58520844206214, - "meanMs" : 0.22371927240715017, - "medianMs" : 0.16016652807593346, - "p95Ms" : 0.3503747284412384, - "p99Ms" : 0.61708316206932068 - }, - "drawsOver16_67ms" : 1, - "fps" : 59.329469831461239, - "frames" : 891, - "framesWithActiveJoins" : 864, - "framesWithRings" : 917, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 70.148124825209379, - "meanMs" : 16.855030102927365, - "medianMs" : 16.672291792929173, - "p95Ms" : 23.504666518419981, - "p99Ms" : 28.268958441913128 - }, - "intervalsOver25ms" : 30, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - { - "atSeconds" : 3.0971020418219268, - "currentDrawMs" : 0.14629168435931206, - "intervalMs" : 27.440916746854782, - "previousDrawMs" : 0.53695822134613991 - }, - { - "atSeconds" : 3.3457603333517909, - "currentDrawMs" : 0.15500001609325409, - "intervalMs" : 26.309208478778601, - "previousDrawMs" : 0.16041658818721771 - }, - { - "atSeconds" : 3.434114208444953, - "currentDrawMs" : 0.21029170602560043, - "intervalMs" : 28.268958441913128, - "previousDrawMs" : 0.16637519001960754 - }, - { - "atSeconds" : 3.7150997500866652, - "currentDrawMs" : 0.21224981173872948, - "intervalMs" : 25.649833492934704, - "previousDrawMs" : 0.18166657537221909 - }, - { - "atSeconds" : 3.9832733748480678, - "currentDrawMs" : 0.26791682466864586, - "intervalMs" : 29.376666527241468, - "previousDrawMs" : 0.22999988868832588 - }, - { - "atSeconds" : 4.2796952500939369, - "currentDrawMs" : 0.2787499688565731, - "intervalMs" : 26.663749944418669, - "previousDrawMs" : 0.22295815870165825 - }, - { - "atSeconds" : 4.3137981668114662, - "currentDrawMs" : 0.22587506100535393, - "intervalMs" : 26.594083290547132, - "previousDrawMs" : 0.220458023250103 - }, - { - "atSeconds" : 4.6366253751330078, - "currentDrawMs" : 0.27270801365375519, - "intervalMs" : 30.755541753023863, - "previousDrawMs" : 0.28804177418351173 - }, - { - "atSeconds" : 5.0147296250797808, - "currentDrawMs" : 0.17558317631483078, - "intervalMs" : 27.756708208471537, - "previousDrawMs" : 0.23999996483325958 - }, - { - "atSeconds" : 5.2455494999885559, - "currentDrawMs" : 0.187708530575037, - "intervalMs" : 26.002458296716213, - "previousDrawMs" : 0.22608321160078049 - }, - { - "atSeconds" : 5.5964336250908673, - "currentDrawMs" : 0.16075000166893005, - "intervalMs" : 26.904208585619926, - "previousDrawMs" : 0.23412518203258514 - }, - { - "atSeconds" : 6.0081990831531584, - "currentDrawMs" : 0.18683355301618576, - "intervalMs" : 38.759916555136442, - "previousDrawMs" : 0.15504192560911179 - }, - { - "atSeconds" : 6.3961527501232922, - "currentDrawMs" : 0.13349996879696846, - "intervalMs" : 26.415125001221895, - "previousDrawMs" : 1.6843331977725029 - }, - { - "atSeconds" : 6.4848350416868925, - "currentDrawMs" : 30.58520844206214, - "intervalMs" : 70.148124825209379, - "previousDrawMs" : 0.2399166114628315 - }, - { - "atSeconds" : 6.5325837498530746, - "currentDrawMs" : 0.18350034952163696, - "intervalMs" : 47.748708166182041, - "previousDrawMs" : 30.58520844206214 - }, - { - "atSeconds" : 6.5950819584541023, - "currentDrawMs" : 0.4864581860601902, - "intervalMs" : 41.990541853010654, - "previousDrawMs" : 0.27495855465531349 - }, - { - "atSeconds" : 6.6624311250634491, - "currentDrawMs" : 0.14108326286077499, - "intervalMs" : 26.229458395391703, - "previousDrawMs" : 0.11404184624552727 - }, - { - "atSeconds" : 9.4790422501973808, - "currentDrawMs" : 0.2193748950958252, - "intervalMs" : 25.962083600461483, - "previousDrawMs" : 0.12066680938005447 - }, - { - "atSeconds" : 10.667131166439503, - "currentDrawMs" : 0.13883365318179131, - "intervalMs" : 28.070291504263878, - "previousDrawMs" : 0.17812522128224373 - }, - { - "atSeconds" : 10.861787416506559, - "currentDrawMs" : 0.18116692081093788, - "intervalMs" : 25.103166699409485, - "previousDrawMs" : 0.1882500946521759 - }, - { - "atSeconds" : 10.933377000037581, - "currentDrawMs" : 0.66608330234885216, - "intervalMs" : 27.817333582788706, - "previousDrawMs" : 2.3276670835912228 - }, - { - "atSeconds" : 11.099929749965668, - "currentDrawMs" : 0.23479154333472252, - "intervalMs" : 25.058166589587927, - "previousDrawMs" : 0.23100012913346291 - }, - { - "atSeconds" : 11.54563962481916, - "currentDrawMs" : 0.36533363163471222, - "intervalMs" : 25.652958080172539, - "previousDrawMs" : 0.58383308351039886 - }, - { - "atSeconds" : 12.028609749861062, - "currentDrawMs" : 0.37941662594676018, - "intervalMs" : 25.312708225101233, - "previousDrawMs" : 0.16774982213973999 - }, - { - "atSeconds" : 12.080036458093673, - "currentDrawMs" : 0.30462536960840225, - "intervalMs" : 26.848291512578726, - "previousDrawMs" : 0.1554582267999649 - }, - { - "atSeconds" : 12.314865708351135, - "currentDrawMs" : 0.18379185348749161, - "intervalMs" : 27.851416729390621, - "previousDrawMs" : 0.2604583278298378 - }, - { - "atSeconds" : 13.075367750134319, - "currentDrawMs" : 0.43141655623912811, - "intervalMs" : 38.763708434998989, - "previousDrawMs" : 0.22983318194746971 - }, - { - "atSeconds" : 13.163771791849285, - "currentDrawMs" : 0.36312500014901161, - "intervalMs" : 26.816667057573795, - "previousDrawMs" : 0.23250002413988113 - }, - { - "atSeconds" : 13.498859791550785, - "currentDrawMs" : 0.16408367082476616, - "intervalMs" : 28.718125075101852, - "previousDrawMs" : 0.29570842161774635 - }, - { - "atSeconds" : 14.949882291723043, - "currentDrawMs" : 0.16191648319363594, - "intervalMs" : 29.852208215743303, - "previousDrawMs" : 0.23774988949298859 - } - ], - "measurementSeconds" : 15, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 61, - "syntheticKeyboardPresses" : 340, - "widthPoints" : 800, - "withBeats" : true, - "withPitchGaps" : true -} \ No newline at end of file diff --git a/docs/validation/staff-join-cadence-control-800.json b/docs/validation/staff-join-cadence-control-800.json deleted file mode 100644 index 448c51d..0000000 --- a/docs/validation/staff-join-cadence-control-800.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "draw" : { - "maxMs" : 0.42212521657347679, - "meanMs" : 0.13553827205517638, - "medianMs" : 0.14391681179404259, - "p95Ms" : 0.23274961858987808, - "p99Ms" : 0.28129154816269875 - }, - "drawsOver16_67ms" : 0, - "fps" : 59.996362015685101, - "frames" : 480, - "framesWithActiveJoins" : 0, - "framesWithRings" : 507, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 24.360458366572857, - "meanMs" : 16.667677279141788, - "medianMs" : 16.663874965161085, - "p95Ms" : 17.124625388532877, - "p99Ms" : 21.50895819067955 - }, - "intervalsOver25ms" : 0, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - - ], - "measurementSeconds" : 8, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 61, - "syntheticKeyboardPresses" : 204, - "widthPoints" : 800, - "withBeats" : true, - "withPitchGaps" : false -} \ No newline at end of file diff --git a/docs/validation/staff-join-cadence-repeat-800.json b/docs/validation/staff-join-cadence-repeat-800.json deleted file mode 100644 index c14e79f..0000000 --- a/docs/validation/staff-join-cadence-repeat-800.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "draw" : { - "maxMs" : 0.43625012040138245, - "meanMs" : 0.15084773864752302, - "medianMs" : 0.15741633251309395, - "p95Ms" : 0.24795811623334885, - "p99Ms" : 0.34991651773452759 - }, - "drawsOver16_67ms" : 0, - "fps" : 59.990811323819507, - "frames" : 480, - "framesWithActiveJoins" : 488, - "framesWithRings" : 507, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 24.49920866638422, - "meanMs" : 16.669219467664501, - "medianMs" : 16.664749942719936, - "p95Ms" : 20.500541664659977, - "p99Ms" : 22.182541899383068 - }, - "intervalsOver25ms" : 0, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - - ], - "measurementSeconds" : 8, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 61, - "syntheticKeyboardPresses" : 204, - "widthPoints" : 800, - "withBeats" : true, - "withPitchGaps" : true -} \ No newline at end of file diff --git a/docs/validation/staff-join-performance.json b/docs/validation/staff-join-performance.json deleted file mode 100644 index 47b53b0..0000000 --- a/docs/validation/staff-join-performance.json +++ /dev/null @@ -1,326 +0,0 @@ -{ - "beatStress" : "180 BPM during typing; artificial pulse every 150ms in capacity case to saturate four pulses and all eight rings", - "burstLimit" : 16, - "method" : "600 measured synthetic frames after 9s warmup per size\/pattern; production keyboard channel, native staff session and retained layer transactions; CLOCK_THREAD_CPUTIME_ID records main-thread CPU, systemUptime separately includes scheduling\/commit waits; excludes WindowServer\/GPU execution and presentation", - "p95BudgetMs" : 2, - "passed" : true, - "pitchGapStress" : "In gap mode, four unvoiced frames in every ten create overlapping joins while new notes continue", - "scenarios" : [ - { - "animationUpdate" : { - "maxMs" : 0.070916488766670227, - "meanMs" : 0.0037014701714118319, - "medianMs" : 0.0034165568649768829, - "p95Ms" : 0.0043753534555435181, - "p99Ms" : 0.0084582716226577759 - }, - "combined" : { - "maxMs" : 2.9464582912623882, - "meanMs" : 0.077417918170491859, - "medianMs" : 0.053208321332931519, - "p95Ms" : 0.120542012155056, - "p99Ms" : 0.17112493515014648 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 2.9435832984745502, - "meanMs" : 0.073716447999080018, - "medianMs" : 0.049666967242956161, - "p95Ms" : 0.11762510985136032, - "p99Ms" : 0.16374979168176651 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakActiveJoins" : 2, - "peakRings" : 5, - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.12066699999999998, - "meanMs" : 0.046233458333333352, - "medianMs" : 0.045000000000000005, - "p95Ms" : 0.05475, - "p99Ms" : 0.068082999999999991 - }, - "threadCPUSubmission" : { - "maxMs" : 0.11783299999999999, - "meanMs" : 0.042805403333333339, - "medianMs" : 0.041667000000000003, - "p95Ms" : 0.050500000000000003, - "p99Ms" : 0.061415999999999998 - }, - "threadCPUUpdate" : { - "maxMs" : 0.023333, - "meanMs" : 0.0034280549999999967, - "medianMs" : 0.0032499999999999999, - "p95Ms" : 0.0041659999999999996, - "p99Ms" : 0.008209000000000001 - }, - "wallP95Under2ms" : true, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 0.03550015389919281, - "meanMs" : 0.0077818077988922596, - "medianMs" : 0.0073746778070926666, - "p95Ms" : 0.0092913396656513214, - "p99Ms" : 0.022083055227994919 - }, - "combined" : { - "maxMs" : 4.0186247788369656, - "meanMs" : 0.078673805886258677, - "medianMs" : 0.0482914038002491, - "p95Ms" : 0.12904172763228416, - "p99Ms" : 0.26354147121310234 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 4.010708536952734, - "meanMs" : 0.070891998087366431, - "medianMs" : 0.040791463106870651, - "p95Ms" : 0.12120837345719337, - "p99Ms" : 0.22804131731390953 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakActiveJoins" : 2, - "peakRings" : 8, - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.13150000000000001, - "meanMs" : 0.041325286666666676, - "medianMs" : 0.039500000000000007, - "p95Ms" : 0.050166999999999996, - "p99Ms" : 0.072832999999999995 - }, - "threadCPUSubmission" : { - "maxMs" : 0.10291700000000001, - "meanMs" : 0.033849110000000036, - "medianMs" : 0.032208000000000001, - "p95Ms" : 0.041500000000000002, - "p99Ms" : 0.060082999999999998 - }, - "threadCPUUpdate" : { - "maxMs" : 0.028583000000000001, - "meanMs" : 0.0074761766666666552, - "medianMs" : 0.007208, - "p95Ms" : 0.0090840000000000001, - "p99Ms" : 0.014666999999999999 - }, - "wallP95Under2ms" : true, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 0.021708663552999496, - "meanMs" : 0.0034966669045388699, - "medianMs" : 0.0033751130104064941, - "p95Ms" : 0.0042081810534000397, - "p99Ms" : 0.0062915496528148651 - }, - "combined" : { - "maxMs" : 2.9214583337306976, - "meanMs" : 0.063533893165489047, - "medianMs" : 0.052750110626220703, - "p95Ms" : 0.11095823720097542, - "p99Ms" : 0.15091663226485252 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 2.9179998673498631, - "meanMs" : 0.06003722626095017, - "medianMs" : 0.049249734729528427, - "p95Ms" : 0.1082918606698513, - "p99Ms" : 0.14704186469316483 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakActiveJoins" : 2, - "peakRings" : 5, - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.15570799999999999, - "meanMs" : 0.045830954999999979, - "medianMs" : 0.044624999999999998, - "p95Ms" : 0.051249999999999997, - "p99Ms" : 0.070958999999999994 - }, - "threadCPUSubmission" : { - "maxMs" : 0.15287499999999998, - "meanMs" : 0.042524093333333263, - "medianMs" : 0.041291000000000001, - "p95Ms" : 0.047458, - "p99Ms" : 0.067667000000000005 - }, - "threadCPUUpdate" : { - "maxMs" : 0.012166, - "meanMs" : 0.0033068616666666584, - "medianMs" : 0.0032499999999999999, - "p95Ms" : 0.0040419999999999996, - "p99Ms" : 0.0056250000000000007 - }, - "wallP95Under2ms" : true, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 0.050167087465524673, - "meanMs" : 0.0071726297028362751, - "medianMs" : 0.0070836395025253296, - "p95Ms" : 0.0077085569500923157, - "p99Ms" : 0.0095418654382228851 - }, - "combined" : { - "maxMs" : 2.6056668721139431, - "meanMs" : 0.055161105313648782, - "medianMs" : 0.045250169932842255, - "p95Ms" : 0.052083283662796021, - "p99Ms" : 0.074875075370073318 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 2.5983750820159912, - "meanMs" : 0.0479884756108125, - "medianMs" : 0.038291327655315399, - "p95Ms" : 0.044708140194416046, - "p99Ms" : 0.068416353315114975 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakActiveJoins" : 2, - "peakRings" : 8, - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.1895, - "meanMs" : 0.038338474999999997, - "medianMs" : 0.037374999999999999, - "p95Ms" : 0.041875000000000002, - "p99Ms" : 0.048000000000000001 - }, - "threadCPUSubmission" : { - "maxMs" : 0.159084, - "meanMs" : 0.031338421666666644, - "medianMs" : 0.030457999999999999, - "p95Ms" : 0.034749999999999996, - "p99Ms" : 0.039040999999999999 - }, - "threadCPUUpdate" : { - "maxMs" : 0.045291999999999999, - "meanMs" : 0.0070000533333333601, - "medianMs" : 0.0069579999999999998, - "p95Ms" : 0.0075830000000000003, - "p99Ms" : 0.0092499999999999995 - }, - "wallP95Under2ms" : true, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 0.024583190679550171, - "meanMs" : 0.003609582781791687, - "medianMs" : 0.0033751130104064941, - "p95Ms" : 0.0043334439396858215, - "p99Ms" : 0.0087916851043701172 - }, - "combined" : { - "maxMs" : 3.0391248874366283, - "meanMs" : 0.060919443300614752, - "medianMs" : 0.051999930292367935, - "p95Ms" : 0.070166774094104767, - "p99Ms" : 0.13587484136223793 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 3.0364999547600746, - "meanMs" : 0.057309860518823065, - "medianMs" : 0.048624817281961441, - "p95Ms" : 0.065291766077280045, - "p99Ms" : 0.12429151684045792 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakActiveJoins" : 2, - "peakRings" : 5, - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.157166, - "meanMs" : 0.046408156666666665, - "medianMs" : 0.044165999999999997, - "p95Ms" : 0.054999999999999993, - "p99Ms" : 0.096124999999999988 - }, - "threadCPUSubmission" : { - "maxMs" : 0.15329100000000001, - "meanMs" : 0.043011209999999987, - "medianMs" : 0.040834000000000002, - "p95Ms" : 0.051083999999999997, - "p99Ms" : 0.090040999999999996 - }, - "threadCPUUpdate" : { - "maxMs" : 0.024, - "meanMs" : 0.0033969466666666577, - "medianMs" : 0.0032499999999999999, - "p95Ms" : 0.0042090000000000001, - "p99Ms" : 0.0067499999999999999 - }, - "wallP95Under2ms" : true, - "widthPoints" : 1600 - }, - { - "animationUpdate" : { - "maxMs" : 1.0212082415819168, - "meanMs" : 0.016185911372303963, - "medianMs" : 0.0079586170613765717, - "p95Ms" : 0.039583537727594376, - "p99Ms" : 0.10075001046061516 - }, - "combined" : { - "maxMs" : 6.8678748793900013, - "meanMs" : 0.17255528286720315, - "medianMs" : 0.056833028793334961, - "p95Ms" : 0.29883300885558128, - "p99Ms" : 3.5184584558010101 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 6.8589583970606327, - "meanMs" : 0.15636937149489918, - "medianMs" : 0.046999659389257431, - "p95Ms" : 0.27779163792729378, - "p99Ms" : 3.5101668909192085 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakActiveJoins" : 2, - "peakRings" : 8, - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.219916, - "meanMs" : 0.057285836666666638, - "medianMs" : 0.043999999999999997, - "p95Ms" : 0.127, - "p99Ms" : 0.16866699999999998 - }, - "threadCPUSubmission" : { - "maxMs" : 0.17691699999999999, - "meanMs" : 0.045970258333333319, - "medianMs" : 0.035541999999999997, - "p95Ms" : 0.1065, - "p99Ms" : 0.15012500000000001 - }, - "threadCPUUpdate" : { - "maxMs" : 0.055458, - "meanMs" : 0.011315578333333338, - "medianMs" : 0.0077919999999999994, - "p95Ms" : 0.032250000000000001, - "p99Ms" : 0.041500000000000002 - }, - "wallP95Under2ms" : true, - "widthPoints" : 1600 - } - ], - "starLimit" : 112, - "typingPressesPerSecond" : 20, - "withBeats" : true, - "withPitchGaps" : true -} \ No newline at end of file diff --git a/docs/validation/staff-join-validation.json b/docs/validation/staff-join-validation.json deleted file mode 100644 index 9fa8705..0000000 --- a/docs/validation/staff-join-validation.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "createdAt": "2026-10-26T10:44:59.271866+08:00", - "scope": "Presentation of reconnection after model-only pitch gaps; pitch recognition, accepted sample times and staff positions are unchanged.", - "behavior": { - "minimumDetachedGapMs": 80, - "durationRangeMs": [ - 189.6, - 300 - ], - "geometry": "Cubic curve with horizontal endpoint tangents, revealed from both ends with smoothstep timing; the complete curve remains afterwards.", - "clock": "Starts at presentation arrival, independently of model latency; long frames use elapsed time.", - "lifecycle": "Real silence still separates phrases; input generation changes and hiding retain existing reset behavior.", - "rendering": "Uses existing trail path and two pencil-stroke layers; no new timer or layer pool." - }, - "tests": { - "staff-note-tests": { - "passed": 92, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 41, - "failed": 0 - } - }, - "totalPassed": 133, - "totalFailed": 0, - "performance": "staff-join-performance.json", - "nativeCadence": { - "initial15SecondRun": "staff-join-cadence-800.json", - "control8SecondRun": "staff-join-cadence-control-800.json", - "repeatedJoin8SecondRun": "staff-join-cadence-repeat-800.json", - "limits": "The initial run recorded 30 intervals above 25ms and one slow submission. Subsequent same-host control and join runs both averaged about 60fps with zero intervals above 25ms. The initial scheduling delay was not conclusively attributed. CPU results exclude GPU and WindowServer execution." - }, - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "04af05dafe0a47397a594e43ddcc3286f9208a895acb4826df019d9bdc6ec0c2", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "03075adee5e11eced6cca175d4d5d53835ee8d83a0a79e790450d16be3a10111", - "tests/visualizations/StaffNoteTests.swift": "ccc64b54bddbd3b90da7ea278c2b2fddf4771f109cbe8265051e88f72db13dfc", - "tests/platforms/macos/StaffNotePlatformTests.swift": "2c8018605f8ce5305941fe163383112233b38aa0e7ad7c87482b048a4717760b", - "tools/macos/PerformanceProbe.swift": "96c325ea5db2a2b74e1363560bcaab8b16f9e70a7c3fb7bde3aa646a75109b43" - }, - "installation": { - "application": "/Applications/Miucs.app", - "binarySHA256": "78b6cc1cfa494406d344d4540e07fb62a738d599dce6a93afdd43ef180a9399e", - "restarted": true, - "runningPID": 5835, - "captureStarted": true, - "windowObserved": true - } -} diff --git a/docs/validation/staff-keyboard-cadence.json b/docs/validation/staff-keyboard-cadence.json deleted file mode 100644 index 4d2d3a9..0000000 --- a/docs/validation/staff-keyboard-cadence.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "draw" : { - "maxMs" : 1.3992083258926868, - "meanMs" : 0.26314899420466986, - "medianMs" : 0.23175030946731567, - "p95Ms" : 0.51745818927884102, - "p99Ms" : 0.73904171586036682 - }, - "drawsOver16_67ms" : 0, - "fps" : 59.86295009462345, - "frames" : 901, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 51.12725030630827, - "meanMs" : 16.704823240741259, - "medianMs" : 16.673708334565163, - "p95Ms" : 21.378833334892988, - "p99Ms" : 32.404750119894743 - }, - "intervalsOver25ms" : 33, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - { - "atSeconds" : 0.043990666512399912, - "currentDrawMs" : 0.10404177010059357, - "intervalMs" : 26.015166658908129, - "previousDrawMs" : 0.15633320435881615 - }, - { - "atSeconds" : 0.097664791624993086, - "currentDrawMs" : 0.18333317711949348, - "intervalMs" : 30.077291652560234, - "previousDrawMs" : 0.112166628241539 - }, - { - "atSeconds" : 1.062330833170563, - "currentDrawMs" : 0.16583362594246864, - "intervalMs" : 27.851916383951902, - "previousDrawMs" : 0.20145811140537262 - }, - { - "atSeconds" : 1.2775177080184221, - "currentDrawMs" : 0.35895872861146927, - "intervalMs" : 26.443041395395994, - "previousDrawMs" : 0.17370842397212982 - }, - { - "atSeconds" : 1.4434077083133161, - "currentDrawMs" : 0.28933305293321609, - "intervalMs" : 25.533125270158052, - "previousDrawMs" : 0.36104209721088409 - }, - { - "atSeconds" : 1.5312734167091548, - "currentDrawMs" : 0.13137469068169594, - "intervalMs" : 45.300875324755907, - "previousDrawMs" : 0.21433364599943161 - }, - { - "atSeconds" : 1.5699165831319988, - "currentDrawMs" : 0.85783330723643303, - "intervalMs" : 35.71354178711772, - "previousDrawMs" : 0.11654198169708252 - }, - { - "atSeconds" : 1.7366313333623111, - "currentDrawMs" : 0.11924980208277702, - "intervalMs" : 34.542249981313944, - "previousDrawMs" : 0.22783316671848297 - }, - { - "atSeconds" : 1.7856927081011236, - "currentDrawMs" : 0.26079174131155014, - "intervalMs" : 33.458124846220016, - "previousDrawMs" : 0.16691675409674644 - }, - { - "atSeconds" : 1.8800214580260217, - "currentDrawMs" : 0.18237531185150146, - "intervalMs" : 28.729916550219059, - "previousDrawMs" : 0.17229188233613968 - }, - { - "atSeconds" : 1.9300745413638651, - "currentDrawMs" : 0.16412511467933655, - "intervalMs" : 44.032000005245209, - "previousDrawMs" : 0.25949999690055847 - }, - { - "atSeconds" : 2.0231887083500624, - "currentDrawMs" : 0.17741648480296135, - "intervalMs" : 35.130958538502455, - "previousDrawMs" : 0.19645830616354942 - }, - { - "atSeconds" : 2.1419810000807047, - "currentDrawMs" : 0.23870822042226791, - "intervalMs" : 51.12725030630827, - "previousDrawMs" : 0.12570852413773537 - }, - { - "atSeconds" : 2.1745318747125566, - "currentDrawMs" : 0.53441664204001427, - "intervalMs" : 25.440083350986242, - "previousDrawMs" : 0.15804171562194824 - }, - { - "atSeconds" : 2.3387061664834619, - "currentDrawMs" : 0.17654197290539742, - "intervalMs" : 33.464124891906977, - "previousDrawMs" : 0.62733329832553864 - }, - { - "atSeconds" : 2.4370450833812356, - "currentDrawMs" : 0.19037490710616112, - "intervalMs" : 32.604374922811985, - "previousDrawMs" : 0.12904126197099686 - }, - { - "atSeconds" : 2.5048698331229389, - "currentDrawMs" : 0.16387505456805229, - "intervalMs" : 32.116166315972805, - "previousDrawMs" : 0.13020820915699005 - }, - { - "atSeconds" : 2.8097091247327626, - "currentDrawMs" : 0.13958336785435677, - "intervalMs" : 25.041582994163036, - "previousDrawMs" : 0.12220814824104309 - }, - { - "atSeconds" : 2.9595145415514708, - "currentDrawMs" : 0.12754183262586594, - "intervalMs" : 25.150374975055456, - "previousDrawMs" : 0.21858327090740204 - }, - { - "atSeconds" : 2.9993692496791482, - "currentDrawMs" : 0.13958336785435677, - "intervalMs" : 31.257624737918377, - "previousDrawMs" : 0.31558331102132797 - }, - { - "atSeconds" : 3.3454452916048467, - "currentDrawMs" : 0.31916657462716103, - "intervalMs" : 27.207375038415194, - "previousDrawMs" : 0.14266651123762131 - }, - { - "atSeconds" : 3.6638983748853207, - "currentDrawMs" : 0.18704170361161232, - "intervalMs" : 28.939333278685808, - "previousDrawMs" : 0.28137490153312683 - }, - { - "atSeconds" : 3.8517976249568164, - "currentDrawMs" : 0.16816658899188042, - "intervalMs" : 32.404750119894743, - "previousDrawMs" : 0.11795852333307266 - }, - { - "atSeconds" : 4.9813609998673201, - "currentDrawMs" : 0.2393750473856926, - "intervalMs" : 29.986166395246983, - "previousDrawMs" : 0.19141659140586853 - }, - { - "atSeconds" : 5.0462353331968188, - "currentDrawMs" : 0.40691671893000603, - "intervalMs" : 28.430583421140909, - "previousDrawMs" : 0.29666675254702568 - }, - { - "atSeconds" : 5.344823416788131, - "currentDrawMs" : 0.18554134294390678, - "intervalMs" : 26.435458566993475, - "previousDrawMs" : 0.20516663789749146 - }, - { - "atSeconds" : 6.8957582917064428, - "currentDrawMs" : 0.25666644796729088, - "intervalMs" : 27.864666655659676, - "previousDrawMs" : 0.37295836955308914 - }, - { - "atSeconds" : 7.3947505000978708, - "currentDrawMs" : 0.21808315068483353, - "intervalMs" : 26.835416909307241, - "previousDrawMs" : 0.62566669657826424 - }, - { - "atSeconds" : 7.8461618749424815, - "currentDrawMs" : 0.37079164758324623, - "intervalMs" : 27.813500259071589, - "previousDrawMs" : 0.16412511467933655 - }, - { - "atSeconds" : 8.0967210833914578, - "currentDrawMs" : 0.20470796152949333, - "intervalMs" : 28.19058345630765, - "previousDrawMs" : 0.2049584873020649 - }, - { - "atSeconds" : 8.7832791251130402, - "currentDrawMs" : 0.30891643837094307, - "intervalMs" : 31.470208428800106, - "previousDrawMs" : 0.25020819157361984 - }, - { - "atSeconds" : 14.299680791795254, - "currentDrawMs" : 0.21645799279212952, - "intervalMs" : 31.17391699925065, - "previousDrawMs" : 0.26895850896835327 - }, - { - "atSeconds" : 14.448212875053287, - "currentDrawMs" : 0.25233300402760506, - "intervalMs" : 29.497458599507809, - "previousDrawMs" : 0.2609998919069767 - } - ], - "measurementSeconds" : 15, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 61, - "syntheticKeyboardPresses" : 341, - "widthPoints" : 800 -} \ No newline at end of file diff --git a/docs/validation/staff-keyboard-performance-wall-variation.json b/docs/validation/staff-keyboard-performance-wall-variation.json deleted file mode 100644 index 515a648..0000000 --- a/docs/validation/staff-keyboard-performance-wall-variation.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "burstLimit" : 16, - "method" : "600 measured synthetic frames after 9s warmup per size\/pattern; production keyboard channel, native staff session and retained layer transactions; no display wait; excludes WindowServer\/GPU presentation", - "p95BudgetMs" : 2, - "passed" : false, - "scenarios" : [ - { - "animationUpdate" : { - "maxMs" : 0.51770824939012527, - "meanMs" : 0.014649999017516773, - "medianMs" : 0.010083429515361786, - "p95Ms" : 0.033333431929349899, - "p99Ms" : 0.055250246077775955 - }, - "combined" : { - "maxMs" : 13.357958756387234, - "meanMs" : 0.82574201825385296, - "medianMs" : 0.1560836099088192, - "p95Ms" : 3.3075413666665554, - "p99Ms" : 12.874458450824022 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 13.316083233803511, - "meanMs" : 0.81109201923633623, - "medianMs" : 0.1449165865778923, - "p95Ms" : 3.2985415309667587, - "p99Ms" : 12.860666960477829 - }, - "p95Under2ms" : false, - "pattern" : "typing20", - "peakStars" : 87, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 3.8245413452386856, - "meanMs" : 0.048688337362060949, - "medianMs" : 0.02366676926612854, - "p95Ms" : 0.075666699558496475, - "p99Ms" : 0.21766684949398041 - }, - "combined" : { - "maxMs" : 30.681958422064781, - "meanMs" : 1.3879982777871192, - "medianMs" : 0.1782500185072422, - "p95Ms" : 12.800208292901516, - "p99Ms" : 19.146124832332134 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 30.647208448499441, - "meanMs" : 1.3393099404250584, - "medianMs" : 0.1471666619181633, - "p95Ms" : 12.781874742358923, - "p99Ms" : 19.102333579212427 - }, - "p95Under2ms" : false, - "pattern" : "capacity", - "peakStars" : 112, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 0.3142915666103363, - "meanMs" : 0.013054774608463049, - "medianMs" : 0.0097085721790790558, - "p95Ms" : 0.029458198696374893, - "p99Ms" : 0.05629146471619606 - }, - "combined" : { - "maxMs" : 14.116874895989895, - "meanMs" : 0.6322181224822998, - "medianMs" : 0.1460416242480278, - "p95Ms" : 0.94999978318810463, - "p99Ms" : 12.337875086814165 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 14.107791241258383, - "meanMs" : 0.61916334787383676, - "medianMs" : 0.13516657054424286, - "p95Ms" : 0.94108330085873604, - "p99Ms" : 12.328458484262228 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakStars" : 87, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 1.1913334019482136, - "meanMs" : 0.036203808461626373, - "medianMs" : 0.021416693925857544, - "p95Ms" : 0.091708265244960785, - "p99Ms" : 0.15245843678712845 - }, - "combined" : { - "maxMs" : 45.559999998658895, - "meanMs" : 1.5323543005312481, - "medianMs" : 0.1504998654127121, - "p95Ms" : 13.358333148062229, - "p99Ms" : 21.605541929602623 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 45.496833045035601, - "meanMs" : 1.496150492069622, - "medianMs" : 0.12479163706302643, - "p95Ms" : 13.339541386812925, - "p99Ms" : 21.555125247687101 - }, - "p95Under2ms" : false, - "pattern" : "capacity", - "peakStars" : 112, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 1.5271664597094059, - "meanMs" : 0.023200344294309616, - "medianMs" : 0.010916497558355331, - "p95Ms" : 0.050249975174665451, - "p99Ms" : 0.18854159861803055 - }, - "combined" : { - "maxMs" : 39.285916835069656, - "meanMs" : 1.0453897225670516, - "medianMs" : 0.16483338549733162, - "p95Ms" : 8.9777917601168156, - "p99Ms" : 15.658416319638491 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 39.097375236451626, - "meanMs" : 1.022189378272742, - "medianMs" : 0.1515829935669899, - "p95Ms" : 8.9577501639723778, - "p99Ms" : 15.611374750733376 - }, - "p95Under2ms" : false, - "pattern" : "typing20", - "peakStars" : 87, - "widthPoints" : 1600 - }, - { - "animationUpdate" : { - "maxMs" : 7.9912915825843811, - "meanMs" : 0.063943970793237284, - "medianMs" : 0.023166649043560028, - "p95Ms" : 0.065666623413562775, - "p99Ms" : 0.14137476682662964 - }, - "combined" : { - "maxMs" : 20.706333685666323, - "meanMs" : 0.92110069779058301, - "medianMs" : 0.16637472435832024, - "p95Ms" : 7.8896665945649147, - "p99Ms" : 13.212375342845917 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 20.679708570241928, - "meanMs" : 0.85715672699734569, - "medianMs" : 0.13483315706253052, - "p95Ms" : 5.8931251987814903, - "p99Ms" : 13.169041834771633 - }, - "p95Under2ms" : false, - "pattern" : "capacity", - "peakStars" : 112, - "widthPoints" : 1600 - } - ], - "starLimit" : 112, - "typingPressesPerSecond" : 20 -} \ No newline at end of file diff --git a/docs/validation/staff-keyboard-performance.json b/docs/validation/staff-keyboard-performance.json deleted file mode 100644 index a81fc4d..0000000 --- a/docs/validation/staff-keyboard-performance.json +++ /dev/null @@ -1,310 +0,0 @@ -{ - "burstLimit" : 16, - "method" : "600 measured synthetic frames after 9s warmup per size\/pattern; production keyboard channel, native staff session and retained layer transactions; CLOCK_THREAD_CPUTIME_ID records main-thread CPU, systemUptime separately includes scheduling\/commit waits; excludes WindowServer\/GPU execution and presentation", - "p95BudgetMs" : 2, - "passed" : true, - "scenarios" : [ - { - "animationUpdate" : { - "maxMs" : 0.016666483134031296, - "meanMs" : 0.0037684136380751929, - "medianMs" : 0.0036251731216907501, - "p95Ms" : 0.0044167973101139069, - "p99Ms" : 0.0063329935073852539 - }, - "combined" : { - "maxMs" : 7.8590414486825466, - "meanMs" : 0.091783327516168356, - "medianMs" : 0.053124967962503433, - "p95Ms" : 0.06079208105802536, - "p99Ms" : 0.71983365342020988 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 7.8556248918175697, - "meanMs" : 0.088014913878093168, - "medianMs" : 0.049499794840812683, - "p95Ms" : 0.056250020861625671, - "p99Ms" : 0.71620848029851913 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.096542000000000003, - "meanMs" : 0.045998958333333374, - "medianMs" : 0.044958000000000005, - "p95Ms" : 0.050500000000000003, - "p99Ms" : 0.073833999999999997 - }, - "threadCPUSubmission" : { - "maxMs" : 0.093167, - "meanMs" : 0.042390894999999977, - "medianMs" : 0.041500000000000002, - "p95Ms" : 0.046625, - "p99Ms" : 0.066833000000000004 - }, - "threadCPUUpdate" : { - "maxMs" : 0.014167000000000001, - "meanMs" : 0.0036080633333333374, - "medianMs" : 0.0035000000000000001, - "p95Ms" : 0.0042500000000000003, - "p99Ms" : 0.0057910000000000001 - }, - "wallP95Under2ms" : true, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 0.057958532124757767, - "meanMs" : 0.0080119453681012, - "medianMs" : 0.0076666474342346191, - "p95Ms" : 0.0092503614723682404, - "p99Ms" : 0.014791730791330338 - }, - "combined" : { - "maxMs" : 3.5493331961333752, - "meanMs" : 0.094021754339337349, - "medianMs" : 0.047000125050544739, - "p95Ms" : 0.054583419114351273, - "p99Ms" : 2.8589167632162571 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 3.5391664132475853, - "meanMs" : 0.08600980897123614, - "medianMs" : 0.039250124245882034, - "p95Ms" : 0.046374741941690445, - "p99Ms" : 2.8513334691524506 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.16033399999999998, - "meanMs" : 0.03990591333333339, - "medianMs" : 0.038542, - "p95Ms" : 0.044083000000000004, - "p99Ms" : 0.074790999999999996 - }, - "threadCPUSubmission" : { - "maxMs" : 0.15287499999999998, - "meanMs" : 0.032119001666666702, - "medianMs" : 0.031041000000000003, - "p95Ms" : 0.035917000000000004, - "p99Ms" : 0.064958000000000002 - }, - "threadCPUUpdate" : { - "maxMs" : 0.024749999999999998, - "meanMs" : 0.0077869116666666582, - "medianMs" : 0.0075000000000000006, - "p95Ms" : 0.0090840000000000001, - "p99Ms" : 0.014542000000000001 - }, - "wallP95Under2ms" : true, - "widthPoints" : 600 - }, - { - "animationUpdate" : { - "maxMs" : 0.061041675508022308, - "meanMs" : 0.004197640810161829, - "medianMs" : 0.0039166770875453949, - "p95Ms" : 0.0051250681281089783, - "p99Ms" : 0.011916738003492355 - }, - "combined" : { - "maxMs" : 4.958166740834713, - "meanMs" : 0.10958068538457155, - "medianMs" : 0.056416727602481842, - "p95Ms" : 0.072749797254800797, - "p99Ms" : 2.9888749122619629 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 4.953041672706604, - "meanMs" : 0.10538304457440972, - "medianMs" : 0.052458606660366058, - "p95Ms" : 0.066791661083698273, - "p99Ms" : 2.9849167913198471 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.125666, - "meanMs" : 0.049379910000000013, - "medianMs" : 0.047708, - "p95Ms" : 0.055833000000000001, - "p99Ms" : 0.088624999999999995 - }, - "threadCPUSubmission" : { - "maxMs" : 0.120708, - "meanMs" : 0.045416315000000006, - "medianMs" : 0.044042000000000005, - "p95Ms" : 0.051792000000000005, - "p99Ms" : 0.0785 - }, - "threadCPUUpdate" : { - "maxMs" : 0.021250000000000002, - "meanMs" : 0.0039635950000000116, - "medianMs" : 0.0037910000000000001, - "p95Ms" : 0.004875, - "p99Ms" : 0.0096249999999999999 - }, - "wallP95Under2ms" : true, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 0.082916580140590668, - "meanMs" : 0.011754306809355816, - "medianMs" : 0.009708106517791748, - "p95Ms" : 0.030125025659799576, - "p99Ms" : 0.046750064939260483 - }, - "combined" : { - "maxMs" : 12.646083254367113, - "meanMs" : 0.24997854294876259, - "medianMs" : 0.060958322137594223, - "p95Ms" : 0.2255002036690712, - "p99Ms" : 4.6364166773855686 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 12.614541687071323, - "meanMs" : 0.23822423613940677, - "medianMs" : 0.050708185881376266, - "p95Ms" : 0.18562516197562218, - "p99Ms" : 4.6059582382440567 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.299958, - "meanMs" : 0.058182983333333362, - "medianMs" : 0.048834000000000002, - "p95Ms" : 0.119958, - "p99Ms" : 0.174458 - }, - "threadCPUSubmission" : { - "maxMs" : 0.29020800000000002, - "meanMs" : 0.046988118333333342, - "medianMs" : 0.039042, - "p95Ms" : 0.098916000000000004, - "p99Ms" : 0.16195900000000002 - }, - "threadCPUUpdate" : { - "maxMs" : 0.069499999999999992, - "meanMs" : 0.011194865000000023, - "medianMs" : 0.0094999999999999998, - "p95Ms" : 0.022957999999999999, - "p99Ms" : 0.041916000000000002 - }, - "wallP95Under2ms" : true, - "widthPoints" : 800 - }, - { - "animationUpdate" : { - "maxMs" : 0.075416639447212219, - "meanMs" : 0.0080689601600170135, - "medianMs" : 0.0066249631345272064, - "p95Ms" : 0.017916783690452576, - "p99Ms" : 0.026000197976827621 - }, - "combined" : { - "maxMs" : 11.381208430975676, - "meanMs" : 0.41147771446655196, - "medianMs" : 0.09199976921081543, - "p95Ms" : 3.1566247344017029, - "p99Ms" : 5.8470834046602249 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 11.363041587173939, - "meanMs" : 0.40340875430653494, - "medianMs" : 0.085291452705860138, - "p95Ms" : 3.1512915156781673, - "p99Ms" : 5.8410833589732647 - }, - "p95Under2ms" : true, - "pattern" : "typing20", - "peakStars" : 87, - "threadCPUCombined" : { - "maxMs" : 0.29341700000000004, - "meanMs" : 0.084661741666666679, - "medianMs" : 0.077084, - "p95Ms" : 0.13041700000000001, - "p99Ms" : 0.19104200000000002 - }, - "threadCPUSubmission" : { - "maxMs" : 0.28549999999999998, - "meanMs" : 0.07704309499999995, - "medianMs" : 0.070250000000000007, - "p95Ms" : 0.11799999999999999, - "p99Ms" : 0.17450000000000002 - }, - "threadCPUUpdate" : { - "maxMs" : 0.075166000000000011, - "meanMs" : 0.0076186466666666739, - "medianMs" : 0.0063749999999999996, - "p95Ms" : 0.015833, - "p99Ms" : 0.023207999999999999 - }, - "wallP95Under2ms" : false, - "widthPoints" : 1600 - }, - { - "animationUpdate" : { - "maxMs" : 0.13749999925494194, - "meanMs" : 0.029393342168380816, - "medianMs" : 0.021958258002996445, - "p95Ms" : 0.06316695362329483, - "p99Ms" : 0.094583258032798767 - }, - "combined" : { - "maxMs" : 18.262541387230158, - "meanMs" : 1.1092493069979052, - "medianMs" : 0.15375018119812012, - "p95Ms" : 9.2907501384615898, - "p99Ms" : 13.258208055049181 - }, - "frames" : 600, - "layerSubmission" : { - "maxMs" : 18.212458118796349, - "meanMs" : 1.0798559648295245, - "medianMs" : 0.12887502089142799, - "p95Ms" : 9.2593333683907986, - "p99Ms" : 13.23274988681078 - }, - "p95Under2ms" : true, - "pattern" : "capacity", - "peakStars" : 112, - "threadCPUCombined" : { - "maxMs" : 0.54591699999999999, - "meanMs" : 0.14764443999999993, - "medianMs" : 0.11770800000000001, - "p95Ms" : 0.316583, - "p99Ms" : 0.390542 - }, - "threadCPUSubmission" : { - "maxMs" : 0.49633299999999997, - "meanMs" : 0.1194411016666666, - "medianMs" : 0.093332999999999999, - "p95Ms" : 0.26683399999999996, - "p99Ms" : 0.35195900000000002 - }, - "threadCPUUpdate" : { - "maxMs" : 0.129333, - "meanMs" : 0.028203338333333324, - "medianMs" : 0.021500000000000002, - "p95Ms" : 0.058541000000000003, - "p99Ms" : 0.083084000000000005 - }, - "wallP95Under2ms" : false, - "widthPoints" : 1600 - } - ], - "starLimit" : 112, - "typingPressesPerSecond" : 20 -} \ No newline at end of file diff --git a/docs/validation/staff-keyboard-platform-tests.json b/docs/validation/staff-keyboard-platform-tests.json deleted file mode 100644 index 7ed8336..0000000 --- a/docs/validation/staff-keyboard-platform-tests.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "checks" : [ - { - "name" : "Native ANSI mapping covers the entire portable layout exactly once", - "passed" : true - }, - { - "name" : "Physical mapping includes both sides and ignores function, navigation and keypad keys", - "passed" : true - }, - { - "name" : "Every left\/right modifier decoder distinguishes press from release with the opposite side held", - "passed" : true - }, - { - "name" : "Fn uses its dedicated down\/up flag", - "passed" : true - }, - { - "name" : "Caps Lock transports both lock states as modifiersChanged", - "passed" : true - }, - { - "name" : "Stars sit above the staff\/trail and below the note and text layers", - "passed" : true - }, - { - "name" : "The retained pool allocates exactly 112 two-stroke vector stars", - "passed" : true - }, - { - "name" : "Cached star geometry is a closed ten-vertex five-point outline", - "passed" : true - }, - { - "name" : "Saturated animation reuses all pool objects and the cached path", - "passed" : true - }, - { - "name" : "Expired particles hide their old layers without removing the pool", - "passed" : true - }, - { - "name" : "600\/800\/1600-point windows scale position and star geometry uniformly", - "passed" : true - }, - { - "name" : "All five global ink colors reach the stars' native strokes", - "passed" : true - }, - { - "name" : "Stars retain transparent interiors without glow or a keyboard background", - "passed" : true - }, - { - "name" : "Native bursts are visible on light\/dark backgrounds at all three sizes", - "passed" : true - }, - { - "name" : "The six rendered size\/background combinations do not clip any particles", - "passed" : true - }, - { - "name" : "The solid note head stays readable when stars pass beneath it", - "passed" : true - }, - { - "name" : "Twenty presses\/sec run alongside pitch, sway, scale and trail with identical audio motion", - "passed" : true - }, - { - "name" : "Fresh silent audio frames stop redrawing after motion and stars settle", - "passed" : true - }, - { - "name" : "A fresh key wakes rendering while the audio channel stays silent", - "passed" : true - }, - { - "name" : "Native reactivation clears cached star presentation", - "passed" : true - } - ], - "failed" : 0, - "passed" : 20 -} \ No newline at end of file diff --git a/docs/validation/staff-keyboard-tests.json b/docs/validation/staff-keyboard-tests.json deleted file mode 100644 index 224a1ae..0000000 --- a/docs/validation/staff-keyboard-tests.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "checks" : [ - { - "name" : "Every main-section physical key has one placement", - "passed" : true - }, - { - "name" : "All five rows span exactly 15 units at the specified heights", - "passed" : true - }, - { - "name" : "Q A Z preserve ANSI row staggering", - "passed" : true - }, - { - "name" : "Wide keys use their centers rather than the left key position", - "passed" : true - }, - { - "name" : "Bottom row includes both modifier sides and the 6.25-unit space", - "passed" : true - }, - { - "name" : "A fresh press produces a center and six stars in the same update", - "passed" : true - }, - { - "name" : "The center rapidly grows while six stars spread in different directions", - "passed" : true - }, - { - "name" : "The center is gone after 100ms, while the six satellites remain", - "passed" : true - }, - { - "name" : "The final expiration requests one redraw to remove stale stars", - "passed" : true - }, - { - "name" : "An idle scene stops requesting keyboard redraws", - "passed" : true - }, - { - "name" : "Simultaneous keys burst at three independent physical positions", - "passed" : true - }, - { - "name" : "Holding does not repeat before 300ms", - "passed" : true - }, - { - "name" : "Holding at 300ms adds one smaller center and four stars", - "passed" : true - }, - { - "name" : "Subsequent repeat interval is 180ms", - "passed" : true - }, - { - "name" : "OS auto-repeat events do not add bursts or change the application rhythm", - "passed" : true - }, - { - "name" : "Release immediately stops births, and existing particles expire naturally", - "passed" : true - }, - { - "name" : "Snapshot cancels a hold even if its release event has aged out", - "passed" : true - }, - { - "name" : "Modifier sides each burst once; releases and holds never repeat", - "passed" : true - }, - { - "name" : "Unresolved modifier transitions are ignored", - "passed" : true - }, - { - "name" : "Caps Lock bursts on both toggles but not duplicate lock states", - "passed" : true - }, - { - "name" : "Keys outside the physical layout do not create particles", - "passed" : true - }, - { - "name" : "Listening midway through a hold never starts synthetic repeats", - "passed" : true - }, - { - "name" : "Event sequence cursor makes replaying a snapshot idempotent", - "passed" : true - }, - { - "name" : "Secure input preserves fading stars but cancels all holds across recovery", - "passed" : true - }, - { - "name" : "Recovery responds to fresh presses in the new generation", - "passed" : true - }, - { - "name" : "Permission loss, suspend, stop and reconnect cannot replay held keys", - "passed" : true - }, - { - "name" : "A long frame expires old particles and emits at most one current repeat", - "passed" : true - }, - { - "name" : "A fresh event after a stall is not aged by the preceding frame duration", - "passed" : true - }, - { - "name" : "Capacity recycles oldest bursts and never exceeds 16 bursts \/ 112 stars", - "passed" : true - }, - { - "name" : "All main-section bursts fit the canvas throughout their lifetime", - "passed" : true - }, - { - "name" : "The native session pipeline animates keyboard input without audio permission", - "passed" : true - }, - { - "name" : "Audio generation changes do not reset keyboard particles or hold rhythm", - "passed" : true - }, - { - "name" : "Keyboard repeat continues independently through audio reconnection", - "passed" : true - }, - { - "name" : "Hiding or disposing the module clears every burst", - "passed" : true - }, - { - "name" : "Resume with a held snapshot does not replay a hidden press", - "passed" : true - }, - { - "name" : "Settled audio and no stars stop display invalidation", - "passed" : true - } - ], - "failed" : 0, - "passed" : 36 -} \ No newline at end of file diff --git a/docs/validation/staff-keyboard-validation.json b/docs/validation/staff-keyboard-validation.json deleted file mode 100644 index 27db118..0000000 --- a/docs/validation/staff-keyboard-validation.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "date": "2026-10-23", - "scope": "Five-line staff keyboard star bursts concurrent with audio; portable layout and simulation, macOS event adaptation and retained rendering", - "status": "implemented; automated checks, native rendering, build/signature, installed app and physical-keyboard acceptance passed", - "tests": { - "contract-tests": { - "passed": 16, - "failed": 0 - }, - "keyboard-channel-tests": { - "passed": 29, - "failed": 0 - }, - "keyboard-engine-tests": { - "passed": 32, - "failed": 0 - }, - "staff-note-tests": { - "passed": 54, - "failed": 0 - }, - "staff-keyboard-tests": { - "passed": 36, - "failed": 0 - }, - "staff-keyboard-platform-tests": { - "passed": 20, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 25, - "failed": 0 - }, - "flight-world-tests": { - "passed": 15, - "failed": 0 - }, - "visualization-host-tests": { - "passed": 14, - "failed": 0 - }, - "visualization-appearance-tests": { - "passed": 27, - "failed": 0 - } - }, - "totalPassed": 268, - "totalFailed": 0, - "dependencyBoundariesAndAudioSchemas": "passed via scripts/test-contracts.sh; no platform imports in shared core", - "rendering": { - "widthsPoints": [ - 600, - 800, - 1600 - ], - "backgrounds": [ - "light", - "dark", - "transparent" - ], - "inkColors": [ - "graphite", - "blue", - "green", - "orange", - "violet" - ], - "fixedPoolStars": 112, - "maxBursts": 16, - "checks": [ - "cached closed five-point outlines", - "pool object reuse", - "uniform scale", - "unclipped particles", - "music above stars", - "idle redraw suspension" - ], - "previewLocation": "build/staff-keyboard-preview/" - }, - "performance": { - "cpuReport": "staff-keyboard-performance.json", - "mainThreadCPUCombinedP95RangeMs": [ - 0.044083000000000004, - 0.316583 - ], - "cpuBudgetMs": 2, - "cpuBudgetPassed": true, - "cpuClock": "CLOCK_THREAD_CPUTIME_ID; main-thread animation update plus retained layer transaction submission", - "wallTimeCaveat": "Wall-clock timings also include scheduling and transaction waits. A separate earlier wall-time run varied substantially; retained in staff-keyboard-performance-wall-variation.json. This is not evidence that CPU execution itself exceeded the budget.", - "wallP95RangeMs": [ - 0.05458341911435127, - 9.29075013846159 - ], - "nativeCadence": [ - { - "report": "staff-keyboard-cadence.json", - "widthPoints": 800, - "seconds": 15, - "fps": 59.86295009462345, - "maxIntervalMs": 51.12725030630827, - "intervalsOver25ms": 33 - }, - { - "report": "staff-keyboard-wide-cadence.json", - "widthPoints": 1600, - "seconds": 10, - "fps": 59.99720346810241, - "maxIntervalMs": 23.13120849430561, - "intervalsOver25ms": 0 - } - ], - "nativeCadenceSource": "Real transparent native window, synthetic simultaneous audio and 20 key presses/sec; display awake and session unlocked; no compilation during measurement", - "hideResumePassed": true, - "limitation": "CPU timings exclude WindowServer/GPU execution. Actual refresh has variable long intervals; neither a low CPU p95 nor a 60fps average guarantees no dropped or late frames." - }, - "realInput": { - "initialDirectExecutableDiagnostic": { - "audioFeatureFrames": 1404, - "audioOnsets": 67, - "audioStates": [ - "running" - ], - "containsKeyCodesOrText": false, - "duration": 30.0072134998627, - "keyboardDroppedEvents": 0, - "keyboardEventsByKind": { - "modifiersChanged": 3, - "pressed": 116, - "released": 117 - }, - "keyboardStates": [ - "running" - ], - "maximumKeyboardDeliveryAgeMs": 21.335500292479992, - "maximumPressesPerSecond": 10, - "simultaneousRunningSamples": 1412 - }, - "initialLaunchServicesDiagnostic": { - "audioFeatureFrames": 232, - "audioOnsets": 9, - "audioStates": [ - "running" - ], - "containsKeyCodesOrText": false, - "duration": 5.008907708339393, - "keyboardDroppedEvents": 0, - "keyboardEventsByKind": {}, - "keyboardStates": [ - "permissionRequired" - ], - "maximumKeyboardDeliveryAgeMs": 0, - "maximumPressesPerSecond": 0, - "simultaneousRunningSamples": 0 - }, - "afterGrantLaunchServicesDiagnostic": { - "audioFeatureFrames": 234, - "audioOnsets": 11, - "audioStates": [ - "running" - ], - "containsKeyCodesOrText": false, - "duration": 5.0146479164250195, - "keyboardDroppedEvents": 0, - "keyboardEventsByKind": {}, - "keyboardStates": [ - "running" - ], - "maximumKeyboardDeliveryAgeMs": 0, - "maximumPressesPerSecond": 0, - "simultaneousRunningSamples": 236 - }, - "installedAppLaunchServicesDiagnostic": { - "audioFeatureFrames": 2108, - "audioOnsets": 114, - "audioStates": [ - "running" - ], - "containsKeyCodesOrText": false, - "duration": 45.00326708331704, - "keyboardDroppedEvents": 0, - "keyboardEventsByKind": { - "modifiersChanged": 8, - "pressed": 19, - "released": 19 - }, - "keyboardStates": [ - "running" - ], - "maximumKeyboardDeliveryAgeMs": 21.05824975296855, - "maximumPressesPerSecond": 15, - "simultaneousRunningSamples": 2115 - }, - "launchContextFinding": "Direct executable launch inherited development-host permission and received events while desktop launch initially reported permissionRequired. Permission was then observed granted in System Settings.", - "versionFinding": "System permission relaunch opened the old /Applications/Miucs.app. Updated the installed bundle with the verified dist build and launched that exact path.", - "containsRawKeysTextOrScreenCapture": false, - "userAcceptance": { - "passed": true, - "confirmation": "看到了,长按和松开也正常", - "scope": [ - "physical typing creates visible bursts", - "holding produces repeated bursts", - "release stops new bursts" - ] - }, - "manualTestLimits": "Left/right combinations, all 61 key positions, all colors/sizes and simulated permission revocation are covered by automated tests, not claimed as individually exercised by the user." - }, - "delivery": { - "installedApp": "/Applications/Miucs.app", - "runningInstalledPID": 89348, - "buildCommand": "zsh build.sh", - "signatureCommand": "codesign --verify --deep --strict --verbose=2 /Applications/Miucs.app", - "signaturePassed": true, - "installedMatchesBuild": true, - "executableSHA256": "929d9ec89a1f73e8f46ccad963506245646ee4943c1a43a4e00bf92024a046bf", - "previousInstalledBundleBackup": "build/Miucs-before-keyboard-stars.app", - "docs": [ - "README.md", - "docs/staff-note.md", - "contracts/keyboard-input/README.md", - "contracts/visualization/README.md", - "docs/architecture.md", - "CONTRIBUTING.md" - ], - "syntheticDemo": "docs/assets/miucs-staff-note.gif", - "demoValidation": "staff-demo.json" - }, - "sourceSHA256": { - "modules/keyboard/core/KeyboardChannel.swift": "364ba9562ad9c2469ec758091784c4538f60668f6d7b899bb449adc1db06307c", - "modules/keyboard/core/KeyboardInput.swift": "cc90e23a4e0659669069b6b5d037f36b894403833074e306466aa2d0b9d85fcf", - "modules/keyboard/platforms/macos/KeyboardEngine.swift": "84afc5e7753110e12fc3b89e8105dd7253563f07b79b6be2e60982c238fe6167", - "modules/keyboard/platforms/macos/KeyboardEventTap.swift": "522a5235a55734f92c926c8c415752ea9e8d6fac194a260d18894b2bc85cac96", - "modules/visualizations/staff-note/core/StaffKeyboardLayout.swift": "aa75549c2c6e8f2dc036540c98848166eff91110574a94b7c828b1f5f7f8e100", - "modules/visualizations/staff-note/core/StaffKeyboardStars.swift": "e4a20201621a7cb0192e41b0eae4f9f5cdb80a9e2d2031ea6f20e7dcf9f397aa", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "aa273b5bf71144ac206c1c57ccdb751255284221a5def66f9a0873827ba11433", - "modules/visualizations/staff-note/core/StaffNoteSimulation.swift": "3dabfba0630a37682696975e4d32c14969f3e76ae94cbc3362cf258a5875e9c1", - "modules/visualizations/staff-note/core/StaffPitch.swift": "ec4f405935a124177615262577ea2e3a002ba2d6c18d60529eaa8c8d897bdc83", - "modules/visualizations/staff-note/core/StaffPitchTracker.swift": "e5e3119d36d4933d47a50f3135f2adcf59273f4555e9842c4659f1836dd2e688", - "modules/visualizations/staff-note/core/StaffVolumeTracker.swift": "7a44f208383dad1b81b025cf6a21576362beee3e04eaf2b2435a6c11e9e0c292", - "modules/visualizations/staff-note/platforms/macos/MacStaffKeyboardLayout.swift": "df80e1b57cd767060657e7954ee735f9532817f6726a3b6fb65b10cfd27e9e92", - "modules/visualizations/staff-note/platforms/macos/MacStaffNoteSession.swift": "762acf18f1348bccdff5a2de207a560522259e72b7fc731f06c778af7f79dc46", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "55f7159720a7127399cb2d3eacde2bb4504769c79fa72b5bb2828c6c8b46f0b9", - "modules/visualizations/api/VisualizationModule.swift": "6b6bb0dd46f2f7f3906166ec47f6d1b7f7017d27890a084e9e4a2760710a116a", - "apps/macos/App.swift": "d594ad94da8cc9521cb4db77f2f4b72e86ff9b6ea68bf091910a7a1beb1130e0", - "apps/macos/VisualizerPanel.swift": "2f2b629056bfe6e92cd83cb453b0765adbcc0aa167f6cdc4e79b80c491f75846", - "apps/macos/VisualizationHostView.swift": "298c4d65f01cdab772cdd5ce1b7c98b1dccb27f40f05002da330c7d4046971c8", - "tools/macos/PerformanceProbe.swift": "f0101492bcd7049441b6aa7acff7ad0a292050bfbbc2af1db848afef89770e8a", - "tools/macos/SyntheticKeyboardInput.swift": "de193fa236b70265c969e90a9f6477b221dd3e3526fdf3046790f837b5bc4c0e", - "tools/macos/StaffNoteDemo.swift": "a52a97777e8efae55b1291f6bcbee847d71b9bc7b04bf35a347a25d2d751e689", - "tools/macos/StaffNotePreview.swift": "33cab4221c7f21a23d6fd78c409eb911733d394be2405c19394ec7c97668d82b", - "tools/macos/InputDiagnostics.swift": "d0901025e9862d0ec14ef97407b8e35cd939d522d795a9ac6d859d452fd68658", - "scripts/test-staff-keyboard.sh": "ac6bc0131ba31f96d7f802228533182bd0e1ffd7109f9be76e5d05c8766fb2e7", - "scripts/measure-performance.sh": "2a627a0372c54d762eeb7d0fac95927bcc499d0493a2c9526791e7179b28fcae", - "scripts/render-demo.sh": "69768d7dc212d4964940c1b5c1790f6319771dd56b4309f0854e7875285bd4d6" - } -} diff --git a/docs/validation/staff-keyboard-wide-cadence.json b/docs/validation/staff-keyboard-wide-cadence.json deleted file mode 100644 index aebc598..0000000 --- a/docs/validation/staff-keyboard-wide-cadence.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "draw" : { - "maxMs" : 0.66858343780040741, - "meanMs" : 0.22685381661479673, - "medianMs" : 0.19620824605226517, - "p95Ms" : 0.42100017890334129, - "p99Ms" : 0.57958345860242844 - }, - "drawsOver16_67ms" : 0, - "fps" : 59.997203468102413, - "frames" : 600, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 23.131208494305611, - "meanMs" : 16.667443517290792, - "medianMs" : 16.671250108629465, - "p95Ms" : 16.979875043034554, - "p99Ms" : 18.127374816685915 - }, - "intervalsOver25ms" : 0, - "keyboardPressesPerSecond" : 20, - "longFrames" : [ - - ], - "measurementSeconds" : 10, - "method" : "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 61, - "syntheticKeyboardPresses" : 244, - "widthPoints" : 1600 -} \ No newline at end of file diff --git a/docs/validation/staff-layout-continuity-validation.json b/docs/validation/staff-layout-continuity-validation.json deleted file mode 100644 index 3a2f27c..0000000 --- a/docs/validation/staff-layout-continuity-validation.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "date": "2026-10-23", - "scope": "Center the displayed musical register and preserve trace continuity through short rejected pitch intervals", - "displayRegister": { - "wholeHistoryOctaveTranslation": true, - "displayOctaves": [ - -1, - 0, - 1, - 2 - ], - "staffCenterY": 74, - "logicalPointsPerOctave": 28, - "acceptedPitchMeanTimeConstantSeconds": 1.2, - "recenterDistanceThresholdPoints": 20, - "recenterConfirmationSeconds": 1, - "behavior": [ - "The first accepted pitch chooses the nearest staff-centered written octave", - "Subsequent sustained register changes move the entire note and trace together with the same critically damped spring", - "The sounding MIDI, true pitch name, pitch filtering and actual melodic intervals remain unchanged", - "A small 8 or 15 by the clef identifies an octave-transposed display", - "Rejected pitches cannot change the display register; fixed staff and keyboard-star geometry are unchanged" - ] - }, - "traceContinuity": { - "maximumShortGapSeconds": 0.35, - "behavior": [ - "Missing, low-confidence, out-of-range or unconfirmed frames add no trace samples", - "Reliable endpoints separated by less than 350ms remain in the same path", - "A visual-only horizontal tail from the last accepted point reaches the current note while the short-gap window remains open", - "Longer gaps and true silence, disconnection or session resets start a new segment" - ] - }, - "tests": { - "staff-note-tests": { - "passed": 74, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 31, - "failed": 0 - }, - "staff-keyboard-tests": { - "passed": 36, - "failed": 0 - }, - "staff-keyboard-platform-tests": { - "passed": 20, - "failed": 0 - }, - "contract-tests": { - "passed": 16, - "failed": 0 - } - }, - "totalPassed": 177, - "totalFailed": 0, - "commands": [ - "zsh scripts/test-staff-note.sh", - "zsh scripts/test-staff-note-platform.sh", - "zsh scripts/test-staff-keyboard.sh", - "zsh scripts/test-contracts.sh", - "zsh build.sh", - "git diff --check" - ], - "additionalChecks": [ - "Dependency-boundary and JSON-schema checks passed", - "Reference and native rendering preserve octave centering, real pitch names, path continuity, keyboard-star and music layer ordering", - "Synthetic 800-point light preview and low G2 preview inspected visually" - ], - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffDisplayRegister.swift": "01c5f1146f59c383b74707abe2b168466832351654189f4c3ce5965c23d033b7", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "1b5ecbfae5f807acffe18fd7e14f0cbc57400cf4548a98edddcb515ee27d7aa6", - "modules/visualizations/staff-note/core/StaffPitch.swift": "381123de7a34d3ce294c5259fa8244c3bd7840ace23e3441b426312b3ccf9642", - "modules/visualizations/staff-note/core/StaffPitchTracker.swift": "3fde5ae30b8d0c9d083fc63b0bd0f0044aab68aa6ef76f6b3ce31adbb259ffe2", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "38e6c2c15c74cfb9737cddb5ea33ef9c580a2645e32d9d042f020426c1c6a531", - "tests/visualizations/StaffNoteTests.swift": "2e1eeea0689fc9b8fc2cc0ef1be01e672ce92044f6961323bc5745e7e31dfa73", - "tests/platforms/macos/StaffNotePlatformTests.swift": "489f095e6653bbbe737d113214f53e6dbaf3536572c7b1f927a420b6c5971f67", - "tests/platforms/macos/StaffKeyboardPlatformTests.swift": "6fbc51eb6e4bec27e2b757a942e1a6ed51e5ce2c15d501e0ba18c64a7b46d2a5", - "tools/macos/StaffNoteDemo.swift": "74841d9738565a25a8f33ed7e142fcaa5f4bf76c2d79e77802d16d5269267845", - "tools/macos/StaffNotePreview.swift": "ec16d5575ba222f825c69f649506ead22e583625c5de79df61e3fbdaaa122f1a" - }, - "delivery": { - "installedApp": "/Applications/Miucs.app", - "signaturePassed": true, - "installedMatchesBuild": true, - "executableSHA256": "30997855df9da74dc6a8a27224cd01fab6b4d3f3f7eba8631e6b242c6cac3274", - "backup": "build/Miucs-before-layout-continuity-20261023.app", - "restartStatus": "running; native staff window and screenshot observed after installation", - "runningInstalledPID": 51012, - "observedWindowTitle": "Miucs · 五线谱音符", - "liveScreenshotObservation": "Idle five-line staff with its note at the middle line; live audio behavior was not claimed from this idle screenshot" - }, - "listeningTrial": "Pending user comparison with the original music; synthetic and native-render checks do not claim vocal isolation or eliminate all instrumental interference" -} diff --git a/docs/validation/staff-neural-continuity.json b/docs/validation/staff-neural-continuity.json deleted file mode 100644 index c1e6129..0000000 --- a/docs/validation/staff-neural-continuity.json +++ /dev/null @@ -1,764 +0,0 @@ -{ - "createdAt": "2026-10-23T18:40:00.930377+00:00", - "method": "Saved gated melody frames; audible accompaniment; production simulation and native vector renderer. Visual continuity only, not a new pitch-accuracy measurement.", - "displayHz": 60, - "modelLatencySeconds": 0.12, - "baselineStaffGitRevision": "a55c496", - "baselineSourceSHA256": { - "modules/visualizations/staff-note/core/StaffPitch.swift": "8964e76630b44271a28df8c6e2b42da2185263c2d5acbd747b0d00ed18e65578", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "9f6a032de793bd366e522b545dee4e3d05d24fff35b77e304b821281a73a7fcf" - }, - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffPitch.swift": "f8a0d10f056f91b3b25aff3df351b6fb88c9e583f9532b88ae381cab8be7c702", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "3b023a55120410ef0e0ca6af9a7d37c4a66af1c20e7ca3268f7fb8bbc1efa180", - "tools/macos/StaffContinuityProbe.swift": "5de436486fe8f82feeab27a9e5236fbcf8de6efcdda3a6e35f8f3688d47b812d" - }, - "before": { - "gatedFramesInStaffRange": 8525, - "gatedFramesBelowOldCutoff": 492, - "tracePoints": 7559, - "segments": 452, - "segmentsShorterThan120ms": 149 - }, - "after": { - "gatedFramesInStaffRange": 8525, - "gatedFramesBelowOldCutoff": 492, - "tracePoints": 7884, - "segments": 270, - "segmentsShorterThan120ms": 49 - }, - "reconnectedGaps": { - "count": 165, - "maximumIntervalMS": 120.0, - "maximumSemitones": 5 - }, - "limits": "Same previously used 31-song-clip cache. Does not measure new audio accuracy or actual desktop frame timing. Brief genuine vocal rests can be visually connected; model output remains unvoiced.", - "tracks": [ - { - "id": "Ani_2_01", - "inputSHA256": "ea96aa87be248f4c2c5d9fb986954dbcad9c4e6917299dc676b5f1754d5c7818", - "before": { - "gatedFramesBelowOldCutoff": 10, - "gatedFramesInStaffRange": 223, - "id": "Ani_2_01", - "inputFrames": 311, - "segments": 18, - "segmentsShorterThan120ms": 9, - "tracePoints": 210 - }, - "after": { - "gatedFramesBelowOldCutoff": 10, - "gatedFramesInStaffRange": 223, - "id": "Ani_2_01", - "inputFrames": 311, - "segments": 9, - "segmentsShorterThan120ms": 3, - "tracePoints": 213 - }, - "reconnectedGaps": 9 - }, - { - "id": "Ani_2_02", - "inputSHA256": "304dd1795117ff550a5d9619f235934579a557945f8b1909a3cfbbe41c9f4530", - "before": { - "gatedFramesBelowOldCutoff": 33, - "gatedFramesInStaffRange": 359, - "id": "Ani_2_02", - "inputFrames": 460, - "segments": 20, - "segmentsShorterThan120ms": 8, - "tracePoints": 302 - }, - "after": { - "gatedFramesBelowOldCutoff": 33, - "gatedFramesInStaffRange": 359, - "id": "Ani_2_02", - "inputFrames": 460, - "segments": 15, - "segmentsShorterThan120ms": 5, - "tracePoints": 323 - }, - "reconnectedGaps": 5 - }, - { - "id": "Ani_2_03", - "inputSHA256": "99d2966296cd0c4ed3f3568a5b816cb84ab4b37e93e75dcc93f4e22c0e4f9b82", - "before": { - "gatedFramesBelowOldCutoff": 18, - "gatedFramesInStaffRange": 281, - "id": "Ani_2_03", - "inputFrames": 351, - "segments": 14, - "segmentsShorterThan120ms": 3, - "tracePoints": 249 - }, - "after": { - "gatedFramesBelowOldCutoff": 18, - "gatedFramesInStaffRange": 281, - "id": "Ani_2_03", - "inputFrames": 351, - "segments": 7, - "segmentsShorterThan120ms": 0, - "tracePoints": 259 - }, - "reconnectedGaps": 6 - }, - { - "id": "Ani_2_04", - "inputSHA256": "9b3d5638ea081eb5c856e04a013cde75c902bdc747b501fdeaf72c78c279346d", - "before": { - "gatedFramesBelowOldCutoff": 21, - "gatedFramesInStaffRange": 259, - "id": "Ani_2_04", - "inputFrames": 316, - "segments": 12, - "segmentsShorterThan120ms": 3, - "tracePoints": 221 - }, - "after": { - "gatedFramesBelowOldCutoff": 21, - "gatedFramesInStaffRange": 259, - "id": "Ani_2_04", - "inputFrames": 316, - "segments": 5, - "segmentsShorterThan120ms": 0, - "tracePoints": 227 - }, - "reconnectedGaps": 6 - }, - { - "id": "Ani_2_05", - "inputSHA256": "3e8b2c76250fc6464ee243128052e0ce62ef0271b4679353d7f5ffd53388a536", - "before": { - "gatedFramesBelowOldCutoff": 19, - "gatedFramesInStaffRange": 403, - "id": "Ani_2_05", - "inputFrames": 565, - "segments": 23, - "segmentsShorterThan120ms": 6, - "tracePoints": 371 - }, - "after": { - "gatedFramesBelowOldCutoff": 19, - "gatedFramesInStaffRange": 403, - "id": "Ani_2_05", - "inputFrames": 565, - "segments": 15, - "segmentsShorterThan120ms": 4, - "tracePoints": 383 - }, - "reconnectedGaps": 7 - }, - { - "id": "Ani_2_06", - "inputSHA256": "1d2812d6d5a38e992319d7d9abca91253fa477bb6f683dc4192a1bb0287d19a7", - "before": { - "gatedFramesBelowOldCutoff": 11, - "gatedFramesInStaffRange": 389, - "id": "Ani_2_06", - "inputFrames": 540, - "segments": 22, - "segmentsShorterThan120ms": 6, - "tracePoints": 357 - }, - "after": { - "gatedFramesBelowOldCutoff": 11, - "gatedFramesInStaffRange": 389, - "id": "Ani_2_06", - "inputFrames": 540, - "segments": 18, - "segmentsShorterThan120ms": 3, - "tracePoints": 368 - }, - "reconnectedGaps": 3 - }, - { - "id": "Ani_2_07", - "inputSHA256": "c442ffbe742c9c9949e85a2f80691beb4aa8218f1edac59a88b0613a832a7e2e", - "before": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 283, - "id": "Ani_2_07", - "inputFrames": 373, - "segments": 8, - "segmentsShorterThan120ms": 2, - "tracePoints": 257 - }, - "after": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 283, - "id": "Ani_2_07", - "inputFrames": 373, - "segments": 6, - "segmentsShorterThan120ms": 1, - "tracePoints": 263 - }, - "reconnectedGaps": 2 - }, - { - "id": "fdps_2_01", - "inputSHA256": "5678521c94260c2422e94e16b8d1b8396af7fa1203a650e92ec6033815405240", - "before": { - "gatedFramesBelowOldCutoff": 16, - "gatedFramesInStaffRange": 275, - "id": "fdps_2_01", - "inputFrames": 376, - "segments": 13, - "segmentsShorterThan120ms": 1, - "tracePoints": 241 - }, - "after": { - "gatedFramesBelowOldCutoff": 16, - "gatedFramesInStaffRange": 275, - "id": "fdps_2_01", - "inputFrames": 376, - "segments": 9, - "segmentsShorterThan120ms": 0, - "tracePoints": 254 - }, - "reconnectedGaps": 4 - }, - { - "id": "fdps_2_02", - "inputSHA256": "deacf6dc44243a2d9351a3cc5e6eb787a843d4bd1de982c06ecec52e139693a2", - "before": { - "gatedFramesBelowOldCutoff": 13, - "gatedFramesInStaffRange": 206, - "id": "fdps_2_02", - "inputFrames": 357, - "segments": 12, - "segmentsShorterThan120ms": 3, - "tracePoints": 183 - }, - "after": { - "gatedFramesBelowOldCutoff": 13, - "gatedFramesInStaffRange": 206, - "id": "fdps_2_02", - "inputFrames": 357, - "segments": 7, - "segmentsShorterThan120ms": 1, - "tracePoints": 192 - }, - "reconnectedGaps": 5 - }, - { - "id": "fdps_2_03", - "inputSHA256": "ef40192c5afad39edadd702d9c537a6eec325ee85c3f4b5216b2bde96adabab0", - "before": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 233, - "id": "fdps_2_03", - "inputFrames": 360, - "segments": 16, - "segmentsShorterThan120ms": 6, - "tracePoints": 212 - }, - "after": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 233, - "id": "fdps_2_03", - "inputFrames": 360, - "segments": 9, - "segmentsShorterThan120ms": 2, - "tracePoints": 221 - }, - "reconnectedGaps": 7 - }, - { - "id": "fdps_2_04", - "inputSHA256": "4cf81bc9f4ed82eedf3fda3e96f2f52b802c0bd72b691a223e1ab8f411b30bf9", - "before": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 254, - "id": "fdps_2_04", - "inputFrames": 346, - "segments": 9, - "segmentsShorterThan120ms": 2, - "tracePoints": 224 - }, - "after": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 254, - "id": "fdps_2_04", - "inputFrames": 346, - "segments": 6, - "segmentsShorterThan120ms": 0, - "tracePoints": 233 - }, - "reconnectedGaps": 3 - }, - { - "id": "fdps_2_05", - "inputSHA256": "dce63f830a31e0554a2b297767f0396d8bdb85e2d4351cecace689e6262c9def", - "before": { - "gatedFramesBelowOldCutoff": 26, - "gatedFramesInStaffRange": 287, - "id": "fdps_2_05", - "inputFrames": 365, - "segments": 19, - "segmentsShorterThan120ms": 8, - "tracePoints": 240 - }, - "after": { - "gatedFramesBelowOldCutoff": 26, - "gatedFramesInStaffRange": 287, - "id": "fdps_2_05", - "inputFrames": 365, - "segments": 14, - "segmentsShorterThan120ms": 4, - "tracePoints": 262 - }, - "reconnectedGaps": 4 - }, - { - "id": "fdps_2_06", - "inputSHA256": "b6187ae9d2789be0ce5532b0ee7c1d1f69b3b565e5115352c59faef8c6b0313d", - "before": { - "gatedFramesBelowOldCutoff": 25, - "gatedFramesInStaffRange": 238, - "id": "fdps_2_06", - "inputFrames": 311, - "segments": 17, - "segmentsShorterThan120ms": 7, - "tracePoints": 201 - }, - "after": { - "gatedFramesBelowOldCutoff": 25, - "gatedFramesInStaffRange": 238, - "id": "fdps_2_06", - "inputFrames": 311, - "segments": 11, - "segmentsShorterThan120ms": 3, - "tracePoints": 209 - }, - "reconnectedGaps": 5 - }, - { - "id": "fdps_2_07", - "inputSHA256": "94296bec1315c81b2e0314afd6471d3d045ab158f6a6fdcc2204225d4b639dc8", - "before": { - "gatedFramesBelowOldCutoff": 14, - "gatedFramesInStaffRange": 299, - "id": "fdps_2_07", - "inputFrames": 434, - "segments": 20, - "segmentsShorterThan120ms": 6, - "tracePoints": 270 - }, - "after": { - "gatedFramesBelowOldCutoff": 14, - "gatedFramesInStaffRange": 299, - "id": "fdps_2_07", - "inputFrames": 434, - "segments": 14, - "segmentsShorterThan120ms": 2, - "tracePoints": 283 - }, - "reconnectedGaps": 4 - }, - { - "id": "fdps_2_08", - "inputSHA256": "ea315611d0913e33bbb0cfacbbd45106f7c63c2e66056049670dadd5de884cb7", - "before": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 264, - "id": "fdps_2_08", - "inputFrames": 311, - "segments": 10, - "segmentsShorterThan120ms": 6, - "tracePoints": 229 - }, - "after": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 264, - "id": "fdps_2_08", - "inputFrames": 311, - "segments": 6, - "segmentsShorterThan120ms": 2, - "tracePoints": 245 - }, - "reconnectedGaps": 3 - }, - { - "id": "fdps_2_09", - "inputSHA256": "0ad115a9433dd423d1dbcb3160fdc2ef3e5a0b2ad387853a922cac9c0b44f965", - "before": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 238, - "id": "fdps_2_09", - "inputFrames": 348, - "segments": 19, - "segmentsShorterThan120ms": 4, - "tracePoints": 210 - }, - "after": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 238, - "id": "fdps_2_09", - "inputFrames": 348, - "segments": 11, - "segmentsShorterThan120ms": 3, - "tracePoints": 217 - }, - "reconnectedGaps": 8 - }, - { - "id": "fdps_2_10", - "inputSHA256": "8644d1df998f6a6e52194ba0c6dc350da8380621fa0ff2c21e552580f10a53e9", - "before": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 215, - "id": "fdps_2_10", - "inputFrames": 330, - "segments": 13, - "segmentsShorterThan120ms": 6, - "tracePoints": 177 - }, - "after": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 215, - "id": "fdps_2_10", - "inputFrames": 330, - "segments": 9, - "segmentsShorterThan120ms": 4, - "tracePoints": 185 - }, - "reconnectedGaps": 3 - }, - { - "id": "fdps_2_11", - "inputSHA256": "851fcded9bb7e5c75e58dbbb79cfc213a38372223630d0f528d9a761dd5cc3e5", - "before": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 269, - "id": "fdps_2_11", - "inputFrames": 389, - "segments": 19, - "segmentsShorterThan120ms": 6, - "tracePoints": 234 - }, - "after": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 269, - "id": "fdps_2_11", - "inputFrames": 389, - "segments": 9, - "segmentsShorterThan120ms": 1, - "tracePoints": 244 - }, - "reconnectedGaps": 10 - }, - { - "id": "fdps_2_12", - "inputSHA256": "d261e6595a637b9e214e95a596926129f5d37d3cfb329f10264f2e664beb2945", - "before": { - "gatedFramesBelowOldCutoff": 17, - "gatedFramesInStaffRange": 252, - "id": "fdps_2_12", - "inputFrames": 384, - "segments": 12, - "segmentsShorterThan120ms": 5, - "tracePoints": 215 - }, - "after": { - "gatedFramesBelowOldCutoff": 17, - "gatedFramesInStaffRange": 252, - "id": "fdps_2_12", - "inputFrames": 384, - "segments": 10, - "segmentsShorterThan120ms": 3, - "tracePoints": 229 - }, - "reconnectedGaps": 3 - }, - { - "id": "geniusturtle_4_01", - "inputSHA256": "0fa4f5b7f3c5192bb79b0ab0436f1f7df35c9a35d7da8ca9c695866b1ed8e826", - "before": { - "gatedFramesBelowOldCutoff": 9, - "gatedFramesInStaffRange": 257, - "id": "geniusturtle_4_01", - "inputFrames": 357, - "segments": 16, - "segmentsShorterThan120ms": 7, - "tracePoints": 233 - }, - "after": { - "gatedFramesBelowOldCutoff": 9, - "gatedFramesInStaffRange": 257, - "id": "geniusturtle_4_01", - "inputFrames": 357, - "segments": 10, - "segmentsShorterThan120ms": 2, - "tracePoints": 242 - }, - "reconnectedGaps": 6 - }, - { - "id": "geniusturtle_4_02", - "inputSHA256": "c9b30e58a324eaa5bce3d71c3c7ad2e43ea19eb1a9bc871982ab58548d60f0f7", - "before": { - "gatedFramesBelowOldCutoff": 9, - "gatedFramesInStaffRange": 199, - "id": "geniusturtle_4_02", - "inputFrames": 327, - "segments": 15, - "segmentsShorterThan120ms": 5, - "tracePoints": 167 - }, - "after": { - "gatedFramesBelowOldCutoff": 9, - "gatedFramesInStaffRange": 199, - "id": "geniusturtle_4_02", - "inputFrames": 327, - "segments": 12, - "segmentsShorterThan120ms": 2, - "tracePoints": 176 - }, - "reconnectedGaps": 3 - }, - { - "id": "geniusturtle_4_03", - "inputSHA256": "a70efa16777ec0dca86a86c47d4f903b7251d9a444f5f3adb9fcaa33dc7d3b9b", - "before": { - "gatedFramesBelowOldCutoff": 10, - "gatedFramesInStaffRange": 261, - "id": "geniusturtle_4_03", - "inputFrames": 365, - "segments": 14, - "segmentsShorterThan120ms": 5, - "tracePoints": 240 - }, - "after": { - "gatedFramesBelowOldCutoff": 10, - "gatedFramesInStaffRange": 261, - "id": "geniusturtle_4_03", - "inputFrames": 365, - "segments": 10, - "segmentsShorterThan120ms": 2, - "tracePoints": 249 - }, - "reconnectedGaps": 4 - }, - { - "id": "geniusturtle_4_04", - "inputSHA256": "ef9252e7b06d81bc8b80aac98c33f44242d95b0459d3197228c8e44ec0669ba0", - "before": { - "gatedFramesBelowOldCutoff": 9, - "gatedFramesInStaffRange": 290, - "id": "geniusturtle_4_04", - "inputFrames": 330, - "segments": 8, - "segmentsShorterThan120ms": 2, - "tracePoints": 274 - }, - "after": { - "gatedFramesBelowOldCutoff": 9, - "gatedFramesInStaffRange": 290, - "id": "geniusturtle_4_04", - "inputFrames": 330, - "segments": 6, - "segmentsShorterThan120ms": 0, - "tracePoints": 281 - }, - "reconnectedGaps": 2 - }, - { - "id": "geniusturtle_4_05", - "inputSHA256": "a79e9ada1705b9b9a3eeaa66378dcb604d1b0c08b8dcd44da0985ff209bfc25f", - "before": { - "gatedFramesBelowOldCutoff": 27, - "gatedFramesInStaffRange": 348, - "id": "geniusturtle_4_05", - "inputFrames": 373, - "segments": 11, - "segmentsShorterThan120ms": 4, - "tracePoints": 300 - }, - "after": { - "gatedFramesBelowOldCutoff": 27, - "gatedFramesInStaffRange": 348, - "id": "geniusturtle_4_05", - "inputFrames": 373, - "segments": 2, - "segmentsShorterThan120ms": 0, - "tracePoints": 326 - }, - "reconnectedGaps": 7 - }, - { - "id": "geniusturtle_4_06", - "inputSHA256": "425866c571f9cbfd82beeecc19ceef0ac5fd5ba58e87296475709cbde7372cc8", - "before": { - "gatedFramesBelowOldCutoff": 22, - "gatedFramesInStaffRange": 283, - "id": "geniusturtle_4_06", - "inputFrames": 328, - "segments": 11, - "segmentsShorterThan120ms": 2, - "tracePoints": 243 - }, - "after": { - "gatedFramesBelowOldCutoff": 22, - "gatedFramesInStaffRange": 283, - "id": "geniusturtle_4_06", - "inputFrames": 328, - "segments": 4, - "segmentsShorterThan120ms": 0, - "tracePoints": 261 - }, - "reconnectedGaps": 5 - }, - { - "id": "geniusturtle_4_07", - "inputSHA256": "3da8c3475019ba2adeff0b8175b81e613fc895078d06ec74e0f859a08e56f7fd", - "before": { - "gatedFramesBelowOldCutoff": 14, - "gatedFramesInStaffRange": 308, - "id": "geniusturtle_4_07", - "inputFrames": 351, - "segments": 11, - "segmentsShorterThan120ms": 5, - "tracePoints": 282 - }, - "after": { - "gatedFramesBelowOldCutoff": 14, - "gatedFramesInStaffRange": 308, - "id": "geniusturtle_4_07", - "inputFrames": 351, - "segments": 7, - "segmentsShorterThan120ms": 1, - "tracePoints": 284 - }, - "reconnectedGaps": 3 - }, - { - "id": "geniusturtle_4_08", - "inputSHA256": "4ba1b7d9874ae5da6ac16d0233fd434f08881218504f1bae348189e46f88b274", - "before": { - "gatedFramesBelowOldCutoff": 11, - "gatedFramesInStaffRange": 272, - "id": "geniusturtle_4_08", - "inputFrames": 356, - "segments": 16, - "segmentsShorterThan120ms": 9, - "tracePoints": 246 - }, - "after": { - "gatedFramesBelowOldCutoff": 11, - "gatedFramesInStaffRange": 272, - "id": "geniusturtle_4_08", - "inputFrames": 356, - "segments": 6, - "segmentsShorterThan120ms": 1, - "tracePoints": 243 - }, - "reconnectedGaps": 8 - }, - { - "id": "geniusturtle_4_09", - "inputSHA256": "6f8b2776477749ed381167bc89eeaf8f1462382788ffd2864a0780d9e1a34dca", - "before": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 284, - "id": "geniusturtle_4_09", - "inputFrames": 365, - "segments": 15, - "segmentsShorterThan120ms": 3, - "tracePoints": 254 - }, - "after": { - "gatedFramesBelowOldCutoff": 15, - "gatedFramesInStaffRange": 284, - "id": "geniusturtle_4_09", - "inputFrames": 365, - "segments": 6, - "segmentsShorterThan120ms": 0, - "tracePoints": 265 - }, - "reconnectedGaps": 8 - }, - { - "id": "geniusturtle_4_10", - "inputSHA256": "8cc196e7cea5b574e0fbb0ac4af40938e1333869a2f2af6ca7284a63ebb9a520", - "before": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 230, - "id": "geniusturtle_4_10", - "inputFrames": 309, - "segments": 10, - "segmentsShorterThan120ms": 3, - "tracePoints": 203 - }, - "after": { - "gatedFramesBelowOldCutoff": 12, - "gatedFramesInStaffRange": 230, - "id": "geniusturtle_4_10", - "inputFrames": 309, - "segments": 4, - "segmentsShorterThan120ms": 0, - "tracePoints": 210 - }, - "reconnectedGaps": 6 - }, - { - "id": "geniusturtle_4_11", - "inputSHA256": "3010366afe40ee0b7b5c7894b99150d37ba5f9a404cf941ef100e89560ee3d07", - "before": { - "gatedFramesBelowOldCutoff": 16, - "gatedFramesInStaffRange": 298, - "id": "geniusturtle_4_11", - "inputFrames": 408, - "segments": 18, - "segmentsShorterThan120ms": 5, - "tracePoints": 269 - }, - "after": { - "gatedFramesBelowOldCutoff": 16, - "gatedFramesInStaffRange": 298, - "id": "geniusturtle_4_11", - "inputFrames": 408, - "segments": 11, - "segmentsShorterThan120ms": 0, - "tracePoints": 280 - }, - "reconnectedGaps": 7 - }, - { - "id": "geniusturtle_4_12", - "inputSHA256": "70e2237cbda2a802cd5ed5620c9cc53749a97ea333a3580cf86555ec94d5c224", - "before": { - "gatedFramesBelowOldCutoff": 19, - "gatedFramesInStaffRange": 268, - "id": "geniusturtle_4_12", - "inputFrames": 358, - "segments": 11, - "segmentsShorterThan120ms": 2, - "tracePoints": 245 - }, - "after": { - "gatedFramesBelowOldCutoff": 19, - "gatedFramesInStaffRange": 268, - "id": "geniusturtle_4_12", - "inputFrames": 358, - "segments": 2, - "segmentsShorterThan120ms": 0, - "tracePoints": 257 - }, - "reconnectedGaps": 9 - } - ], - "verification": { - "core": 86, - "native": 40, - "featureChannel": 16, - "failures": 0, - "build": "zsh build.sh --rapidly", - "codeSignature": "codesign --verify --deep --strict passed", - "installedBinarySHA256": "daf264b064fdb1336c94991344a279195f63fb218867daf94cd90a173b3597c0", - "processID": 65113, - "restartedAt": "2026-10-23T18:41:32.330290+00:00", - "smokeTest": "Played geniusturtle_4_01 mixed WAV via afplay at volume 0.25. Native window visibly displayed C♯3 and live trace. Startup log confirmed Rapidly trial, aligned vocal-energy gate, and 48kHz stereo capture.", - "limits": "Live playback smoke test only; no new desktop cadence benchmark or manual keyboard acceptance in this change." - } -} diff --git a/docs/validation/staff-note-cadence.json b/docs/validation/staff-note-cadence.json deleted file mode 100644 index ce3a7a3..0000000 --- a/docs/validation/staff-note-cadence.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "draw" : { - "maxMs" : 2.5497497990727425, - "meanMs" : 0.25748812551896333, - "medianMs" : 0.24445820599794388, - "p95Ms" : 0.4276251420378685, - "p99Ms" : 0.74816681444644928 - }, - "fps" : 59.496740491158249, - "frames" : 358, - "hiddenStopsRendering" : true, - "interval" : { - "maxMs" : 61.486583203077316, - "meanMs" : 16.807643439704215, - "medianMs" : 16.665416304022074, - "p95Ms" : 16.918166540563107, - "p99Ms" : 17.794874962419271 - }, - "intervalsOver25ms" : 1, - "method" : "real native transparent window; synthetic capture transport; 1s warmup + 6s measurement; no recording", - "moduleID" : "staff-note", - "resumedFramesInOneSecond" : 57, - "widthPoints" : 1000 -} \ No newline at end of file diff --git a/docs/validation/staff-note-platform-tests.json b/docs/validation/staff-note-platform-tests.json deleted file mode 100644 index 0e9cda2..0000000 --- a/docs/validation/staff-note-platform-tests.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "checks" : [ - { - "name" : "Legacy flight preference keys and 400x260 layout remain compatible", - "passed" : true - }, - { - "name" : "First staff switch uses 800x160 and keeps the old upper-left anchor", - "passed" : true - }, - { - "name" : "New session activates only after its own viewport is restored", - "passed" : true - }, - { - "name" : "Module selection is persisted", - "passed" : true - }, - { - "name" : "The long panel stays transparent and mouse click-through", - "passed" : true - }, - { - "name" : "Staff scaling uses the 5:1 ratio and logical 800-point width", - "passed" : true - }, - { - "name" : "Switching back restores flight's independent position and size", - "passed" : true - }, - { - "name" : "Returning to staff restores its independent position and size", - "passed" : true - }, - { - "name" : "Same or unknown module leaves its current geometry\/session intact", - "passed" : true - }, - { - "name" : "Reopening remembers the selected module and its saved rectangle", - "passed" : true - }, - { - "name" : "Staff has a readable minimum width of 600 points", - "passed" : true - }, - { - "name" : "Staff maximum is screen-constrained and keeps its ratio within one backing pixel", - "passed" : true - }, - { - "name" : "Long-layout resizing keeps the upper-left anchor and accepts adjustment input", - "passed" : true - }, - { - "name" : "Finishing adjustment restores mouse transparency", - "passed" : true - }, - { - "name" : "Changing a hidden module does not activate rendering", - "passed" : true - }, - { - "name" : "Unknown saved identifiers fall back to flight", - "passed" : true - }, - { - "name" : "All three production modules are registered and staff has no palette panel", - "passed" : true - }, - { - "name" : "First redraw after native resume cannot display a cached old trajectory", - "passed" : true - }, - { - "name" : "Production drawing tilts the stem around an unchanged solid head", - "passed" : true - }, - { - "name" : "Retained native layers preserve the same pivot and rotating stem", - "passed" : true - }, - { - "name" : "Native and reference renderers place low and high notes at fixed absolute staff heights", - "passed" : true - }, - { - "name" : "Fixed staff positions keep their true sounding pitch labels", - "passed" : true - }, - { - "name" : "The treble clef has no changing octave-transposition label", - "passed" : true - }, - { - "name" : "The rendered C4 head uses the same height after low and high musical contexts", - "passed" : true - }, - { - "name" : "A later low phrase leaves old C4 pixels at their original staff height", - "passed" : true - }, - { - "name" : "The rendered live contour reaches the note while a brief invalid sample is filtered", - "passed" : true - }, - { - "name" : "The native trace has one continuous subpath across a short filtered interval", - "passed" : true - }, - { - "name" : "The native contour joins live motion but stops extending when pitch becomes stale", - "passed" : true - }, - { - "name" : "A model-only gap joins progressively and settles into one continuous native subpath", - "passed" : true - }, - { - "name" : "The native trace still separates actual silence", - "passed" : true - }, - { - "name" : "A pitch dropout draws only existing history, with no held-note horizontal extension", - "passed" : true - }, - { - "name" : "An 80ms pitch gap starts joining from its real endpoints while the live path reaches the note", - "passed" : true - }, - { - "name" : "Overlapping pitch gap joins settle back into one native path", - "passed" : true - }, - { - "name" : "Native reconnection grows curved tips before closing the gap, with a smooth final curve", - "passed" : true - }, - { - "name" : "High and low notes have no extra short horizontal lines in either renderer at all three sizes", - "passed" : true - }, - { - "name" : "Removing ledger strokes preserves all five main staff lines", - "passed" : true - }, - { - "name" : "Identical scene redraws have byte-identical pencil marks", - "passed" : true - }, - { - "name" : "Canvas remains mostly transparent with antialiased pencil lines", - "passed" : true - }, - { - "name" : "Graphite and pale underlines are visible on light and dark backgrounds", - "passed" : true - }, - { - "name" : "G2 and C6 note bodies, stems and flags remain inside the canvas", - "passed" : true - }, - { - "name" : "Minimum and maximum sizes render uniformly at 2x backing scale", - "passed" : true - } - ], - "failed" : 0, - "passed" : 41 -} \ No newline at end of file diff --git a/docs/validation/staff-note-tests.json b/docs/validation/staff-note-tests.json deleted file mode 100644 index b863a83..0000000 --- a/docs/validation/staff-note-tests.json +++ /dev/null @@ -1,375 +0,0 @@ -{ - "checks" : [ - { - "name" : "Natural notes alternate exact staff lines and spaces", - "passed" : true - }, - { - "name" : "Sharps share the natural note position and use sharp spelling", - "passed" : true - }, - { - "name" : "Octaves preserve seven diatonic steps and 28 points", - "passed" : true - }, - { - "name" : "E2 and C7 fit the fixed 160-point canvas", - "passed" : true - }, - { - "name" : "Absent, invalid, low confidence and out-of-range pitches are rejected", - "passed" : true - }, - { - "name" : "Staff following accepts G2 through C6 and rejects neighboring notes without clamping", - "passed" : true - }, - { - "name" : "Hysteresis prevents semitone boundary chatter", - "passed" : true - }, - { - "name" : "Low-confidence candidates cannot bypass the mixed-spectrum cutoff", - "passed" : true - }, - { - "name" : "The melody gate rejects unvoiced, nonfinite and out-of-range observations", - "passed" : true - }, - { - "name" : "First missing melody waits faintly on B4", - "passed" : true - }, - { - "name" : "Fresh targets preserve position and movement starts on the next refresh", - "passed" : true - }, - { - "name" : "Accepted targets and repeated notes settle within 150ms", - "passed" : true - }, - { - "name" : "High-confidence intrusions lasting up to five frames leave no pitch spikes", - "passed" : true - }, - { - "name" : "Brief out-of-range bursts hold the last pitch and add no trace or edge notes", - "passed" : true - }, - { - "name" : "Returning from a brief rejected range keeps a continuous trace without adding rejected samples", - "passed" : true - }, - { - "name" : "Persistent out-of-range sound expires the label without moving or extending the trail", - "passed" : true - }, - { - "name" : "Out-of-range sound at startup does not invent a boundary note", - "passed" : true - }, - { - "name" : "Unconfirmed jumps pause the trail instead of appending held or guessed pitches", - "passed" : true - }, - { - "name" : "Confirmed jumps join the previous reliable point across a short confirmation interval", - "passed" : true - }, - { - "name" : "Missing pitch expires its live tail even while accompaniment continues", - "passed" : true - }, - { - "name" : "Pitch after a model-only gap reconnects the same audible session", - "passed" : true - }, - { - "name" : "Real silence still breaks the trace immediately", - "passed" : true - }, - { - "name" : "A returning delayed pitch starts a fresh visual join while the new note is immediately accepted", - "passed" : true - }, - { - "name" : "Join growth advances between analysis frames and duplicate snapshots cannot restart it", - "passed" : true - }, - { - "name" : "A long frame settles a pending visual join without adding or moving any pitch samples", - "passed" : true - }, - { - "name" : "Filtering gaps keep one session contour while stale pitch never draws a held-note tail", - "passed" : true - }, - { - "name" : "A continuous visual tail expires with its eight-second history instead of persisting indefinitely", - "passed" : true - }, - { - "name" : "Semitone and whole-tone vocal motion changes the target on its first frame", - "passed" : true - }, - { - "name" : "Sustained thirds, fifths and octaves require 100, 125 and 150ms and confirm within one hop", - "passed" : true - }, - { - "name" : "Confirmed jumps keep their full staff distance and the trail reaches the true note", - "passed" : true - }, - { - "name" : "A melodic phrase with thirds and fourths keeps notes sustained beyond confirmation", - "passed" : true - }, - { - "name" : "Unrelated high and low candidates cannot accumulate into a confirmed jump", - "passed" : true - }, - { - "name" : "A sustained new register can confirm despite nearby semitone variation", - "passed" : true - }, - { - "name" : "Repeated snapshots cannot confirm a jump or append trajectory points", - "passed" : true - }, - { - "name" : "Widely spaced observations cannot be treated as continuous confirmation", - "passed" : true - }, - { - "name" : "Unconfirmed candidates do not keep an old reliable pitch alive indefinitely", - "passed" : true - }, - { - "name" : "Label expiry cannot bypass jump confirmation during continuing sound", - "passed" : true - }, - { - "name" : "A sustained new pitch can confirm after the old label expires", - "passed" : true - }, - { - "name" : "A brief missing result holds the old note and cannot turn a new jump into an immediate note", - "passed" : true - }, - { - "name" : "Missing melody clears pending candidates but a confirmed note joins the audible phrase", - "passed" : true - }, - { - "name" : "A rejected range interrupts candidate continuity rather than joining two short bursts", - "passed" : true - }, - { - "name" : "The trail softens its first corner and reaches 95 percent of a small change within 90ms", - "passed" : true - }, - { - "name" : "Every accepted G2-C6 pitch settles at its fixed absolute treble-staff height", - "passed" : true - }, - { - "name" : "C4 returns to the same height after low\/high phrases and input resets", - "passed" : true - }, - { - "name" : "Holding C4 for four more seconds never recenters the note or its target", - "passed" : true - }, - { - "name" : "A later low phrase cannot shift existing C4 history or compress its octave distance", - "passed" : true - }, - { - "name" : "Repeated snapshots cannot adapt the volume range without new audio", - "passed" : true - }, - { - "name" : "Steady sustained sound does not produce artificial size pulses", - "passed" : true - }, - { - "name" : "Invalid audio cannot corrupt the adaptive volume range", - "passed" : true - }, - { - "name" : "One onset sways across both sides and decays back to the center", - "passed" : true - }, - { - "name" : "Rotational sway keeps the solid head center fixed at its exact staff position", - "passed" : true - }, - { - "name" : "Successive onsets alternate their initial rotation direction", - "passed" : true - }, - { - "name" : "Ordinary musical dynamics visibly change size while preserving pitch and base motion", - "passed" : true - }, - { - "name" : "Dense onsets preserve a fixed pivot, continuous rotation and bounded tilt", - "passed" : true - }, - { - "name" : "Trace preserves the accepted note and ignores loudness and decorative sway", - "passed" : true - }, - { - "name" : "Trace capacity never exceeds 512 samples", - "passed" : true - }, - { - "name" : "History moves left in real time and fades", - "passed" : true - }, - { - "name" : "All history expires after eight seconds, including long scheduling gaps", - "passed" : true - }, - { - "name" : "Unreliable pitch holds the last height and fades the label", - "passed" : true - }, - { - "name" : "Sound without melody holds pitch, stops history and still follows onsets", - "passed" : true - }, - { - "name" : "Silence stops kicks while existing sway settles smoothly", - "passed" : true - }, - { - "name" : "Snapshot and event duplicates do not replay actions or trajectory", - "passed" : true - }, - { - "name" : "Cached melody expires even when the snapshot remains fresh", - "passed" : true - }, - { - "name" : "Stale audio cannot move the note or start actions", - "passed" : true - }, - { - "name" : "Reconnect isolates old history and accepts the new generation", - "passed" : true - }, - { - "name" : "Audio format changes discard old history and queued kicks", - "passed" : true - }, - { - "name" : "PCM timeline reset does not connect unrelated trajectories", - "passed" : true - }, - { - "name" : "Hidden session stops consuming and clears hidden history", - "passed" : true - }, - { - "name" : "Resume rejects an expired hidden snapshot", - "passed" : true - }, - { - "name" : "Resume starts a fresh trajectory from current reliable input", - "passed" : true - }, - { - "name" : "Disconnected state holds height and lets existing history fade", - "passed" : true - }, - { - "name" : "Brief missing melody keeps the reliable trajectory connected", - "passed" : true - }, - { - "name" : "Explicit absent pitch stops the label, sway and live tail while accompaniment continues", - "passed" : true - }, - { - "name" : "A pitch gap does not create history or reposition the last note", - "passed" : true - }, - { - "name" : "Pitch resumes at its real pitch and reconnects the visual line across a pitch gap", - "passed" : true - }, - { - "name" : "40-120ms pitch dropouts reconnect real endpoints even with 120-200ms input latency", - "passed" : true - }, - { - "name" : "Pending pitch gaps never append held notes, extend a live tail or allow new sway", - "passed" : true - }, - { - "name" : "Pitch gaps up to two seconds and large note changes preserve a continuous visual contour without invented samples", - "passed" : true - }, - { - "name" : "Raw-audio silence cancels a pending trace bridge immediately", - "passed" : true - }, - { - "name" : "Duplicate or older unvoiced observations cannot reopen a gap or replay a note", - "passed" : true - }, - { - "name" : "Stalled pitch with live audio reconnects history without treating the old pitch as reliable", - "passed" : true - }, - { - "name" : "An actual audio stall still breaks the visual session", - "passed" : true - }, - { - "name" : "Continuous music with no pitch still expires all history after eight seconds", - "passed" : true - }, - { - "name" : "A return after history expiry draws only the new real point", - "passed" : true - }, - { - "name" : "Switch through the real channel consumes current pitch without old bursts", - "passed" : true - }, - { - "name" : "New channel onsets animate immediately while a large pitch jump is pending", - "passed" : true - }, - { - "name" : "A sustained octave is confirmed through the production feature channel", - "passed" : true - }, - { - "name" : "Hidden channel events are skipped and only current pitch starts fresh history", - "passed" : true - }, - { - "name" : "Long render stalls expire old history while retaining the fresh current sample", - "passed" : true - } - ], - "failed" : 0, - "maximumRemainingFractionAt150ms" : 0.017351562048803712, - "passed" : 89, - "pitchConfirmationMillisecondsAt48kHz" : { - "60->64" : 106.66666666666667, - "60->67" : 128, - "60->72" : 170.66666666666669, - "69->64" : 106.66666666666667, - "72->60" : 170.66666666666669, - "72->65" : 128 - }, - "syntheticIntrusions" : { - "filteredVerticalTravel" : 0, - "rawVerticalTravel" : 384 - } -} \ No newline at end of file diff --git a/docs/validation/staff-note-validation.json b/docs/validation/staff-note-validation.json deleted file mode 100644 index bec3cf3..0000000 --- a/docs/validation/staff-note-validation.json +++ /dev/null @@ -1,200 +0,0 @@ -{ - "date": "2026-10-23T06:13:41.931527+00:00", - "scope": "staff-note visualization, per-module layout and geometry; no audio algorithm or JSON contract changes", - "status": "passed", - "regressions": { - "passed": 206, - "failed": 0, - "reports": [ - { - "path": "docs/validation/audio-feature-tests.json", - "passed": 28, - "failed": 0 - }, - { - "path": "docs/validation/melody-tests.json", - "passed": 14, - "failed": 0 - }, - { - "path": "docs/validation/audio-recovery-tests.json", - "passed": 18, - "failed": 0 - }, - { - "path": "docs/validation/flight-world-tests.json", - "passed": 15, - "failed": 0 - }, - { - "path": "docs/validation/flight-window-tests.json", - "passed": 19, - "failed": 0 - }, - { - "path": "docs/validation/pencil-palette-tests.json", - "passed": 25, - "failed": 0 - }, - { - "path": "docs/validation/contract-tests.json", - "passed": 16, - "failed": 0 - }, - { - "path": "docs/validation/audio-pipeline-tests.json", - "passed": 3, - "failed": 0 - }, - { - "path": "docs/validation/visualization-host-tests.json", - "passed": 11, - "failed": 0 - }, - { - "path": "docs/validation/staff-note-tests.json", - "passed": 34, - "failed": 0 - }, - { - "path": "docs/validation/staff-note-platform-tests.json", - "passed": 23, - "failed": 0 - } - ] - }, - "numericalReference": { - "status": "passed", - "frames": 542, - "mismatches": 0, - "path": "docs/validation/numpy-reference.json" - }, - "flightPreviewByteEquality": [ - { - "name": "flight-transparent.png", - "passed": true, - "SHA256": "7b8cab0269a6094b42dea8c3a21955848795f5f0e14ed3d1d4fda71dd91519f2" - }, - { - "name": "flight-light.png", - "passed": true, - "SHA256": "cf2b9d0f933d36b915de75735297f591b246509b4dcc6edc517f982713d09dac" - }, - { - "name": "flight-dark.png", - "passed": true, - "SHA256": "6e18d7113a602893e498f9d077caf529be04da6f97c1c0eaf177814385371cfd" - } - ], - "staffPreviews": { - "method": "invented notes through production renderer, 2x bitmap; no desktop or audio capture", - "widthsPoints": [ - 600, - 800, - 1600 - ], - "backgrounds": [ - "transparent", - "light", - "dark" - ], - "edgeNotes": [ - "E2", - "C7" - ], - "staticRedrawByteEquality": true, - "readmeAsset": "docs/assets/miucs-staff-note.png" - }, - "compatibility": { - "bundleID": "com.miucs.app", - "signingIdentity": "Miucs Dev", - "nativeBuild": "passed", - "strictSignatureVerification": "passed", - "xcodeSourcePaths": "validated; no Xcode project generation/build run", - "audioAndJSONContractsUnchanged": true, - "legacyFlightPreferenceKeysRetained": true, - "perModuleGeometryAndSelection": "passed isolated-preference tests" - }, - "pending": [], - "sourceSHA256": { - "apps/macos/App.swift": "fe4ead2f42a5c39c9270eb62ea7d3963d960fd5b434fe5c842d814c00f6d790f", - "apps/macos/BuiltInVisualizations.swift": "fbc1879ee57edbf6d3b4335a523ae1405bb6f17f4d3755c0f0dc9552fd53435e", - "apps/macos/VisualizationHostView.swift": "3faad6c4345f0dba02afcf336a8f9c1cfbd0cb932358d6c17bae7856b15acd68", - "apps/macos/VisualizerPanel.swift": "f8d74c37d9a692b22222f9b89df8174d013bc05de04c205fd57363f9ccfa9c48", - "modules/visualizations/api/VisualizationModule.swift": "878e0f12bd91ae262b484956612090b1450e73799973c5eac6bb89b152d5f146", - "modules/visualizations/pencil-flight/core/PencilFlightSimulation.swift": "b8aa2023354664db56460980c8dc0ce523ad3f206927b60675299a5418376de3", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "3cedd4c7e43a75f427ad16d7322f96d96b1d74a0eb5db47f59e1ef72e82703b3", - "modules/visualizations/staff-note/core/StaffNoteSimulation.swift": "4a98d554b4571163c6ebdfb87dbc077926080cedeb10eb3fdcd5d28c8bbb2cdf", - "modules/visualizations/staff-note/core/StaffPitch.swift": "ec4f405935a124177615262577ea2e3a002ba2d6c18d60529eaa8c8d897bdc83", - "modules/visualizations/staff-note/platforms/macos/MacStaffNoteSession.swift": "d978011f4c8071428269fbdca1297d059f21d46dafcf1b7ed268938078f1c1d2", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "46ded4b6bb4d6b2b6992e01ff4a188a47ea251e503cecc58d29f4254eb2398e6" - }, - "nativeCadence": { - "staff-note": { - "path": "docs/validation/staff-note-cadence.json", - "draw": { - "maxMs": 2.5497497990727425, - "meanMs": 0.25748812551896333, - "medianMs": 0.24445820599794388, - "p95Ms": 0.4276251420378685, - "p99Ms": 0.7481668144464493 - }, - "fps": 59.49674049115825, - "frames": 358, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 61.486583203077316, - "meanMs": 16.807643439704215, - "medianMs": 16.665416304022074, - "p95Ms": 16.918166540563107, - "p99Ms": 17.79487496241927 - }, - "intervalsOver25ms": 1, - "method": "real native transparent window; synthetic capture transport; 1s warmup + 6s measurement; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 57, - "widthPoints": 1000 - }, - "pencil-flight": { - "path": "docs/validation/flight-cadence-staff-regression.json", - "draw": { - "maxMs": 3.4489580430090427, - "meanMs": 0.47509719138996226, - "medianMs": 0.4182918928563595, - "p95Ms": 0.8967914618551731, - "p99Ms": 1.80395832285285 - }, - "fps": 59.66226935612995, - "frames": 358, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 53.26800001785159, - "meanMs": 16.761011788386757, - "medianMs": 16.669124830514193, - "p95Ms": 17.37295836210251, - "p99Ms": 21.244083531200886 - }, - "intervalsOver25ms": 2, - "method": "real native transparent window; synthetic capture transport; 1s warmup + 6s measurement; no recording", - "moduleID": "pencil-flight", - "resumedFramesInOneSecond": 57, - "widthPoints": 1000 - } - }, - "interactiveVerification": { - "method": "CUA inspected the running native application; observed flight to staff mode change, restarted from this workspace build, and inspected the restored staff image and the disabled settings menu. Window sizing/resize constraints are covered by isolated native platform tests.", - "checks": { - "selectedStaffAfterRestart": true, - "staffGeometryPreserved": true, - "flightGeometryPreserved": true, - "flightWidthPreserved": true, - "nativeStaffImageObservedAfterRestart": true, - "settingsMenuDisabledWithoutPalette": true - }, - "activeModule": "staff-note", - "staffWidthPoints": 800.0, - "staffHeightPoints": 160.0, - "syntheticAssetsOnly": true, - "nativeUIScreenshotsPublished": false - } -} diff --git a/docs/validation/staff-pitch-filter-validation.json b/docs/validation/staff-pitch-filter-validation.json deleted file mode 100644 index c7f4c85..0000000 --- a/docs/validation/staff-pitch-filter-validation.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "date": "2026-10-23", - "scope": "Narrow staff pitch following and reject short excursions while audio motion and keyboard stars continue independently", - "followingRange": { - "notes": "G2-C6", - "midi": [ - 43, - 84 - ], - "notationY": [ - 138, - 42 - ], - "outsideBehavior": "reject; never clamp to an edge" - }, - "jumpConfirmationMinimumMs": { - "3-5 semitones": 100, - "6-10 semitones": 125, - "11+ semitones": 150 - }, - "measuredConfirmationMsAt48kHz1024Hop": { - "60->64": 106.66666666666667, - "60->67": 128, - "60->72": 170.66666666666669, - "69->64": 106.66666666666667, - "72->60": 170.66666666666669, - "72->65": 128 - }, - "behavior": [ - "Changes of one or two semitones respond on the first valid frame", - "Pending, missing, low-confidence or out-of-range results add no trace points", - "Return from an interruption starts a fresh trace segment at the true notation height", - "Previous accepted pitch remains the comparison reference through continuing sound even after the 240ms label expiry", - "Silence, disconnection, format or session changes and hiding reset the comparison reference", - "Shared melody extraction and flight pitch handling are unchanged" - ], - "tests": { - "staff-note-tests": { - "passed": 64, - "failed": 0 - }, - "staff-note-platform-tests": { - "passed": 25, - "failed": 0 - }, - "staff-keyboard-tests": { - "passed": 36, - "failed": 0 - }, - "staff-keyboard-platform-tests": { - "passed": 20, - "failed": 0 - }, - "contract-tests": { - "passed": 16, - "failed": 0 - } - }, - "totalPassed": 161, - "totalFailed": 0, - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffPitch.swift": "381123de7a34d3ce294c5259fa8244c3bd7840ace23e3441b426312b3ccf9642", - "modules/visualizations/staff-note/core/StaffPitchTracker.swift": "3fde5ae30b8d0c9d083fc63b0bd0f0044aab68aa6ef76f6b3ce31adbb259ffe2", - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "b7b887bd18b2ea96220832409c9b999ed3de3234c35693f233e3856ffbe154e5", - "modules/visualizations/staff-note/core/StaffNoteSimulation.swift": "3dabfba0630a37682696975e4d32c14969f3e76ae94cbc3362cf258a5875e9c1", - "tests/visualizations/StaffNoteTests.swift": "24b2bfadc51fb5d63cfec9a440262640acd3d7e17571f878224b3c089fa929dd", - "tests/platforms/macos/StaffNotePlatformTests.swift": "a55d6754548f28a7cd284ee6606d3850f957f92f7220a5bceea0e2a83e0cd83c", - "tools/macos/StaffNotePreview.swift": "ec16d5575ba222f825c69f649506ead22e583625c5de79df61e3fbdaaa122f1a" - }, - "delivery": { - "buildCommand": "zsh build.sh", - "signaturePassed": true, - "installedApp": "/Applications/Miucs.app", - "installedMatchesBuild": true, - "executableSHA256": "d318d65fa16a485b4df88bb298433128acae2ad318cc08d4faa629ed504b94cd", - "backup": "build/Miucs-before-pitch-filter-20261023.app", - "restartStatus": "running; native staff window observed after user unlocked the desktop", - "runningInstalledPID": 18471, - "observedWindowTitle": "Miucs · 五线谱音符" - }, - "listeningTrial": "pending user comparison with the problematic music; no claim of vocal separation or removal of all instrumental interference" -} diff --git a/docs/validation/staff-render-performance.json b/docs/validation/staff-render-performance.json deleted file mode 100644 index 6ed7273..0000000 --- a/docs/validation/staff-render-performance.json +++ /dev/null @@ -1,388 +0,0 @@ -{ - "date": "2026-10-23", - "method": "Sequential native transparent-window runs with identical synthetic 47Hz feature transport, 1s warmup, 60Hz displays, and no concurrent compilation. Normal Miucs remained running. Runs are not simultaneous; OS scheduling and system load vary.", - "changes": [ - "Skip output-device probes when PCM is fresh; otherwise query Core Audio off the main actor and reject cancelled or stale-generation results.", - "Prepare note labels and fixed vector paths outside the frame loop.", - "Use retained shape, gradient-mask and text layers in the native window; keep Core Graphics drawing for offline exports." - ], - "cpuMetric": "The draw fields measure CPU draw/submission callback time: Core Graphics commands before, retained-layer property submission after. They do not include WindowServer GPU execution or physical presentation.", - "cases": [ - { - "widthPoints": 640, - "measurementSeconds": 15, - "stages": { - "before": { - "draw": { - "maxMs": 1.492250245064497, - "meanMs": 0.3171540758340986, - "medianMs": 0.31245825812220573, - "p95Ms": 0.5330829881131649, - "p99Ms": 0.7229582406580448 - }, - "drawsOver16_67ms": 0, - "fps": 59.796472761652524, - "frames": 897, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 61.287999618798494, - "meanMs": 16.723394438096356, - "medianMs": 16.667666845023632, - "p95Ms": 16.96379156783223, - "p99Ms": 19.477583467960358 - }, - "intervalsOver25ms": 2, - "longFrames": [ - { - "atSeconds": 1.0916116666048765, - "currentDrawMs": 0.1360834576189518, - "intervalMs": 61.287999618798494, - "previousDrawMs": 0.2343747764825821 - }, - { - "atSeconds": 9.37640625005588, - "currentDrawMs": 0.7388750091195107, - "intervalMs": 28.934708330780268, - "previousDrawMs": 0.25683362036943436 - } - ], - "measurementSeconds": 15, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 61, - "widthPoints": 640 - }, - "cachedDrawing": { - "draw": { - "maxMs": 1.2329164892435074, - "meanMs": 0.23950051043373438, - "medianMs": 0.23366650566458702, - "p95Ms": 0.4005003720521927, - "p99Ms": 0.5196668207645416 - }, - "drawsOver16_67ms": 0, - "fps": 59.7954205018739, - "frames": 898, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 67.32679158449173, - "meanMs": 16.723688730789366, - "medianMs": 16.672125086188316, - "p95Ms": 17.056583426892757, - "p99Ms": 19.692708272486925 - }, - "intervalsOver25ms": 2, - "longFrames": [ - { - "atSeconds": 1.0891982084140182, - "currentDrawMs": 0.09033316746354103, - "intervalMs": 67.32679158449173, - "previousDrawMs": 0.2276250161230564 - }, - { - "atSeconds": 2.2136757913976908, - "currentDrawMs": 0.10950025171041489, - "intervalMs": 25.204333011060953, - "previousDrawMs": 0.15083327889442444 - } - ], - "measurementSeconds": 15, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 60, - "widthPoints": 640 - }, - "asyncDeviceChecks": { - "draw": { - "maxMs": 3.1159999780356884, - "meanMs": 0.17500626709403466, - "medianMs": 0.15129195526242256, - "p95Ms": 0.30062487348914146, - "p99Ms": 0.4791663959622383 - }, - "drawsOver16_67ms": 0, - "fps": 59.86349388218925, - "frames": 898, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 51.54933361336589, - "meanMs": 16.704671497590663, - "medianMs": 16.668041702359915, - "p95Ms": 16.828291583806276, - "p99Ms": 21.534166764467955 - }, - "intervalsOver25ms": 5, - "longFrames": [ - { - "atSeconds": 2.587706916499883, - "currentDrawMs": 0.16479194164276123, - "intervalMs": 25.53049987182021, - "previousDrawMs": 0.25524990633130074 - }, - { - "atSeconds": 2.9134477502666414, - "currentDrawMs": 1.9433749839663506, - "intervalMs": 51.54933361336589, - "previousDrawMs": 0.12020859867334366 - }, - { - "atSeconds": 11.176385583356023, - "currentDrawMs": 0.3332914784550667, - "intervalMs": 30.70108313113451, - "previousDrawMs": 0.276416540145874 - }, - { - "atSeconds": 11.611027750186622, - "currentDrawMs": 3.1159999780356884, - "intervalMs": 25.39812494069338, - "previousDrawMs": 0.1922082155942917 - }, - { - "atSeconds": 14.544201583601534, - "currentDrawMs": 0.40841661393642426, - "intervalMs": 31.744458712637424, - "previousDrawMs": 0.14733336865901947 - } - ], - "measurementSeconds": 15, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 61, - "widthPoints": 640 - }, - "retainedLayers": { - "draw": { - "maxMs": 4.95179183781147, - "meanMs": 0.06043896612429063, - "medianMs": 0.053666532039642334, - "p95Ms": 0.09012501686811447, - "p99Ms": 0.1255837269127369 - }, - "drawsOver16_67ms": 0, - "fps": 59.996965820678035, - "frames": 901, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 29.74441647529602, - "meanMs": 16.667509536879756, - "medianMs": 16.667999792844057, - "p95Ms": 16.81429147720337, - "p99Ms": 21.149500273168087 - }, - "intervalsOver25ms": 4, - "longFrames": [ - { - "atSeconds": 5.866144291590899, - "currentDrawMs": 0.0982498750090599, - "intervalMs": 29.74441647529602, - "previousDrawMs": 0.06258301436901093 - }, - { - "atSeconds": 10.544975750148296, - "currentDrawMs": 0.04650000482797623, - "intervalMs": 25.215625297278166, - "previousDrawMs": 0.016458332538604736 - }, - { - "atSeconds": 10.6629647500813, - "currentDrawMs": 0.05358317866921425, - "intervalMs": 26.410041842609644, - "previousDrawMs": 0.03900006413459778 - }, - { - "atSeconds": 11.4318384998478, - "currentDrawMs": 4.95179183781147, - "intervalMs": 28.625333216041327, - "previousDrawMs": 0.03808317705988884 - } - ], - "measurementSeconds": 15, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 62, - "widthPoints": 640 - } - } - }, - { - "widthPoints": 1000, - "measurementSeconds": 6, - "stages": { - "before": { - "draw": { - "maxMs": 0.4514167085289955, - "meanMs": 0.171072736861153, - "medianMs": 0.16579125076532364, - "p95Ms": 0.30858302488923073, - "p99Ms": 0.388250220566988 - }, - "drawsOver16_67ms": 0, - "fps": 59.66156849904029, - "frames": 358, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 49.68841653317213, - "meanMs": 16.761208683544517, - "medianMs": 16.66645798832178, - "p95Ms": 16.845958307385445, - "p99Ms": 17.311207950115204 - }, - "intervalsOver25ms": 1, - "longFrames": [ - { - "atSeconds": 1.079424750059843, - "currentDrawMs": 0.13258308172225952, - "intervalMs": 49.68841653317213, - "previousDrawMs": 0.12170802801847458 - } - ], - "measurementSeconds": 6, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 60, - "widthPoints": 1000 - }, - "cachedDrawing": { - "draw": { - "maxMs": 0.4225415177643299, - "meanMs": 0.1370004617593475, - "medianMs": 0.12679165229201317, - "p95Ms": 0.27841655537486076, - "p99Ms": 0.30841678380966187 - }, - "drawsOver16_67ms": 0, - "fps": 59.66324069838559, - "frames": 358, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 51.95470852777362, - "meanMs": 16.76073891217677, - "medianMs": 16.66824985295534, - "p95Ms": 16.848375089466572, - "p99Ms": 17.097666393965483 - }, - "intervalsOver25ms": 1, - "longFrames": [ - { - "atSeconds": 1.0808966252952814, - "currentDrawMs": 0.0705416314303875, - "intervalMs": 51.95470852777362, - "previousDrawMs": 0.21370826289057732 - } - ], - "measurementSeconds": 6, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 61, - "widthPoints": 1000 - }, - "asyncDeviceChecks": { - "draw": { - "maxMs": 0.9362916462123394, - "meanMs": 0.12692395040450666, - "medianMs": 0.11733314022421837, - "p95Ms": 0.21866662427783012, - "p99Ms": 0.3831246867775917 - }, - "drawsOver16_67ms": 0, - "fps": 59.99540493627043, - "frames": 361, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 30.117583461105824, - "meanMs": 16.66794317101853, - "medianMs": 16.669041477143764, - "p95Ms": 16.964041627943516, - "p99Ms": 20.926916506141424 - }, - "intervalsOver25ms": 3, - "longFrames": [ - { - "atSeconds": 1.1302321669645607, - "currentDrawMs": 0.49795815721154213, - "intervalMs": 25.219833478331566, - "previousDrawMs": 0.13262499123811722 - }, - { - "atSeconds": 3.547281333245337, - "currentDrawMs": 0.15295855700969696, - "intervalMs": 25.495624635368586, - "previousDrawMs": 0.12066634371876717 - }, - { - "atSeconds": 3.5868971250019968, - "currentDrawMs": 0.19524991512298584, - "intervalMs": 30.117583461105824, - "previousDrawMs": 0.12470874935388565 - } - ], - "measurementSeconds": 6, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 60, - "widthPoints": 1000 - }, - "retainedLayers": { - "draw": { - "maxMs": 0.12962520122528076, - "meanMs": 0.04043459581832091, - "medianMs": 0.03458373248577118, - "p95Ms": 0.07600011304020882, - "p99Ms": 0.09583309292793274 - }, - "drawsOver16_67ms": 0, - "fps": 59.99629577242727, - "frames": 360, - "hiddenStopsRendering": true, - "interval": { - "maxMs": 28.137874789536, - "meanMs": 16.66769568229867, - "medianMs": 16.66733343154192, - "p95Ms": 16.767041757702827, - "p99Ms": 16.87679160386324 - }, - "intervalsOver25ms": 1, - "longFrames": [ - { - "atSeconds": 4.121346083004028, - "currentDrawMs": 0.03458373248577118, - "intervalMs": 28.137874789536, - "previousDrawMs": 0.026499852538108826 - } - ], - "measurementSeconds": 6, - "method": "real native transparent window; synthetic capture transport; 1s warmup; no recording", - "moduleID": "staff-note", - "resumedFramesInOneSecond": 61, - "widthPoints": 1000 - } - } - } - ], - "functionalChecks": { - "staff-note-platform-tests": { - "passed": 25, - "failed": 0 - }, - "visualization-appearance-tests": { - "passed": 27, - "failed": 0 - }, - "audio-recovery-tests": { - "passed": 20, - "failed": 0 - }, - "visualization-host-tests": { - "passed": 11, - "failed": 0 - } - }, - "remainingJitter": "Final 640pt/15s run still had 4 intervals above 25ms (maximum 29.74ms); 1000pt/6s had 1 (28.14ms). The repeated early 50-70ms pause was absent in these final runs. This is not a zero-drop-frame guarantee.", - "sourcesSHA256": { - "modules/audio/platforms/macos/AudioEngine.swift": "59932ef473a2723ce462e117383556d2c980be1cfd4f57264d564e00b8b71a53", - "modules/visualizations/staff-note/platforms/macos/StaffNoteView.swift": "71e29ce116e96f138fe6ee34bd71f10a10a60ba68b209a798cdad0cbc137b8dd", - "tools/macos/PerformanceProbe.swift": "92cd0853a4ce0e60c24f08de55c4d2025b2d6e2d6dca42cb0f8f0792d42a0f0a", - "tools/macos/StaffNotePreview.swift": "6d19a63b25c71c5d6b0ff0faded96baa65a1d5b8d3c5078aaf42b622b92dd594", - "scripts/measure-performance.sh": "388972c717fd9d5e770fd32a0167534809e6690d46f4728d3ae8dbac79e3f504" - } -} diff --git a/docs/validation/staff-session-continuity-after.png b/docs/validation/staff-session-continuity-after.png deleted file mode 100644 index 83250e3..0000000 Binary files a/docs/validation/staff-session-continuity-after.png and /dev/null differ diff --git a/docs/validation/staff-session-continuity-before.png b/docs/validation/staff-session-continuity-before.png deleted file mode 100644 index f9a6924..0000000 Binary files a/docs/validation/staff-session-continuity-before.png and /dev/null differ diff --git a/docs/validation/staff-session-continuity.json b/docs/validation/staff-session-continuity.json deleted file mode 100644 index 575f51a..0000000 --- a/docs/validation/staff-session-continuity.json +++ /dev/null @@ -1,313 +0,0 @@ -{ - "createdAt": "2026-10-24T13:17:47.873281+00:00", - "method": "Saved gated melody frames; audible accompaniment; production simulation and native vector renderer. Visual continuity only, not a new pitch-accuracy measurement.", - "displayHz": 60, - "modelLatencySeconds": 0.12, - "sourceSHA256": { - "modules/visualizations/staff-note/core/StaffNoteScene.swift": "ecdad1e1414081824dc932422be4d47aa2d9bab5dc1ed2206f67f5d44ed52dcb", - "modules/visualizations/staff-note/core/StaffPitch.swift": "f8a0d10f056f91b3b25aff3df351b6fb88c9e583f9532b88ae381cab8be7c702", - "tools/macos/StaffContinuityProbe.swift": "7956307ebd1bec5c1fdd3cfb4f586a91aa9f1585dc65c75f1c8be36ba241b894" - }, - "previousSceneSHA256": "3b023a55120410ef0e0ca6af9a7d37c4a66af1c20e7ca3268f7fb8bbc1efa180", - "previousPitchSHA256": "f8a0d10f056f91b3b25aff3df351b6fb88c9e583f9532b88ae381cab8be7c702", - "beforeSegments": 270, - "afterSegments": 31, - "acceptedPoints": 7884, - "acceptedPitchAndTimeUnchanged": true, - "allClipsHaveOneNativeSubpath": true, - "limits": "Saved frames from 31 previously used clips, with audible accompaniment throughout. No new audio-accuracy or desktop-cadence benchmark. Visual lines bridge vocal rests and model dropouts; they do not assert voiced pitch during the gap.", - "tracks": [ - { - "id": "Ani_2_01", - "inputSHA256": "ea96aa87be248f4c2c5d9fb986954dbcad9c4e6917299dc676b5f1754d5c7818", - "beforeSegments": 9, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 213, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "Ani_2_02", - "inputSHA256": "304dd1795117ff550a5d9619f235934579a557945f8b1909a3cfbbe41c9f4530", - "beforeSegments": 15, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 323, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "Ani_2_03", - "inputSHA256": "99d2966296cd0c4ed3f3568a5b816cb84ab4b37e93e75dcc93f4e22c0e4f9b82", - "beforeSegments": 7, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 259, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "Ani_2_04", - "inputSHA256": "9b3d5638ea081eb5c856e04a013cde75c902bdc747b501fdeaf72c78c279346d", - "beforeSegments": 5, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 227, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "Ani_2_05", - "inputSHA256": "3e8b2c76250fc6464ee243128052e0ce62ef0271b4679353d7f5ffd53388a536", - "beforeSegments": 15, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 383, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "Ani_2_06", - "inputSHA256": "1d2812d6d5a38e992319d7d9abca91253fa477bb6f683dc4192a1bb0287d19a7", - "beforeSegments": 18, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 368, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "Ani_2_07", - "inputSHA256": "c442ffbe742c9c9949e85a2f80691beb4aa8218f1edac59a88b0613a832a7e2e", - "beforeSegments": 6, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 263, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_01", - "inputSHA256": "5678521c94260c2422e94e16b8d1b8396af7fa1203a650e92ec6033815405240", - "beforeSegments": 9, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 254, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_02", - "inputSHA256": "deacf6dc44243a2d9351a3cc5e6eb787a843d4bd1de982c06ecec52e139693a2", - "beforeSegments": 7, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 192, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_03", - "inputSHA256": "ef40192c5afad39edadd702d9c537a6eec325ee85c3f4b5216b2bde96adabab0", - "beforeSegments": 9, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 221, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_04", - "inputSHA256": "4cf81bc9f4ed82eedf3fda3e96f2f52b802c0bd72b691a223e1ab8f411b30bf9", - "beforeSegments": 6, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 233, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_05", - "inputSHA256": "dce63f830a31e0554a2b297767f0396d8bdb85e2d4351cecace689e6262c9def", - "beforeSegments": 14, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 262, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_06", - "inputSHA256": "b6187ae9d2789be0ce5532b0ee7c1d1f69b3b565e5115352c59faef8c6b0313d", - "beforeSegments": 11, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 209, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_07", - "inputSHA256": "94296bec1315c81b2e0314afd6471d3d045ab158f6a6fdcc2204225d4b639dc8", - "beforeSegments": 14, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 283, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_08", - "inputSHA256": "ea315611d0913e33bbb0cfacbbd45106f7c63c2e66056049670dadd5de884cb7", - "beforeSegments": 6, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 245, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_09", - "inputSHA256": "0ad115a9433dd423d1dbcb3160fdc2ef3e5a0b2ad387853a922cac9c0b44f965", - "beforeSegments": 11, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 217, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_10", - "inputSHA256": "8644d1df998f6a6e52194ba0c6dc350da8380621fa0ff2c21e552580f10a53e9", - "beforeSegments": 9, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 185, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_11", - "inputSHA256": "851fcded9bb7e5c75e58dbbb79cfc213a38372223630d0f528d9a761dd5cc3e5", - "beforeSegments": 9, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 244, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "fdps_2_12", - "inputSHA256": "d261e6595a637b9e214e95a596926129f5d37d3cfb329f10264f2e664beb2945", - "beforeSegments": 10, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 229, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_01", - "inputSHA256": "0fa4f5b7f3c5192bb79b0ab0436f1f7df35c9a35d7da8ca9c695866b1ed8e826", - "beforeSegments": 10, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 242, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_02", - "inputSHA256": "c9b30e58a324eaa5bce3d71c3c7ad2e43ea19eb1a9bc871982ab58548d60f0f7", - "beforeSegments": 12, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 176, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_03", - "inputSHA256": "a70efa16777ec0dca86a86c47d4f903b7251d9a444f5f3adb9fcaa33dc7d3b9b", - "beforeSegments": 10, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 249, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_04", - "inputSHA256": "ef9252e7b06d81bc8b80aac98c33f44242d95b0459d3197228c8e44ec0669ba0", - "beforeSegments": 6, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 281, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_05", - "inputSHA256": "a79e9ada1705b9b9a3eeaa66378dcb604d1b0c08b8dcd44da0985ff209bfc25f", - "beforeSegments": 2, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 326, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_06", - "inputSHA256": "425866c571f9cbfd82beeecc19ceef0ac5fd5ba58e87296475709cbde7372cc8", - "beforeSegments": 4, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 261, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_07", - "inputSHA256": "3da8c3475019ba2adeff0b8175b81e613fc895078d06ec74e0f859a08e56f7fd", - "beforeSegments": 7, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 284, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_08", - "inputSHA256": "4ba1b7d9874ae5da6ac16d0233fd434f08881218504f1bae348189e46f88b274", - "beforeSegments": 6, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 243, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_09", - "inputSHA256": "6f8b2776477749ed381167bc89eeaf8f1462382788ffd2864a0780d9e1a34dca", - "beforeSegments": 6, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 265, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_10", - "inputSHA256": "8cc196e7cea5b574e0fbb0ac4af40938e1333869a2f2af6ca7284a63ebb9a520", - "beforeSegments": 4, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 210, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_11", - "inputSHA256": "3010366afe40ee0b7b5c7894b99150d37ba5f9a404cf941ef100e89560ee3d07", - "beforeSegments": 11, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 280, - "acceptedPitchAndTimeUnchanged": true - }, - { - "id": "geniusturtle_4_12", - "inputSHA256": "70e2237cbda2a802cd5ed5620c9cc53749a97ea333a3580cf86555ec94d5c224", - "beforeSegments": 2, - "afterSegments": 1, - "nativeVisibleSubpaths": 1, - "acceptedPoints": 257, - "acceptedPitchAndTimeUnchanged": true - } - ], - "verification": { - "corePassed": 89, - "nativePassed": 40, - "failed": 0, - "build": "zsh build.sh --rapidly", - "signature": "codesign --verify --deep --strict passed", - "installedBinarySHA256": "bd46f6ee7a72fceb9bae8a5c81437008bc42cf15e3f314abd4cb66608fab6a66", - "processID": 2870, - "restartedAt": "2026-10-24T13:20:45.811128+00:00", - "livePlayback": "Repeated geniusturtle_4_01 mixed WAV at volume 0.25. Native app screenshot visibly showed one connected trace leading to a C♯3 note. Test playback stopped after observation.", - "capture": "Startup log: Rapidly trial, aligned vocal-energy gate, 48kHz stereo, maximum separator latency 96ms.", - "limits": "Live smoke test, not a new pitch-accuracy, physical-keyboard, or desktop-frame-cadence benchmark." - } -} diff --git a/docs/validation/visualization-appearance-tests.json b/docs/validation/visualization-appearance-tests.json index e510d3c..2c2498b 100644 --- a/docs/validation/visualization-appearance-tests.json +++ b/docs/validation/visualization-appearance-tests.json @@ -40,18 +40,6 @@ "name" : "Unknown saved ink safely falls back to graphite", "passed" : true }, - { - "name" : "Legacy flight ink migrates to the nearest shared color without losing its original data", - "passed" : true - }, - { - "name" : "An explicit shared selection takes precedence over a legacy palette", - "passed" : true - }, - { - "name" : "Corrupt legacy data cannot enter the shared color model", - "passed" : true - }, { "name" : "Native opacity menu actions apply, mark and save ten levels from 10 to 100 percent", "passed" : true @@ -69,7 +57,7 @@ "passed" : true }, { - "name" : "All three modes inherit transparency while adjustment overlays remain opaque", + "name" : "All four modes inherit transparency while adjustment overlays remain opaque", "passed" : true }, { @@ -90,5 +78,5 @@ } ], "failed" : 0, - "passed" : 22 + "passed" : 19 } \ No newline at end of file diff --git a/docs/windows-acceptance.md b/docs/windows-acceptance.md index 8491c57..5ef937a 100644 --- a/docs/windows-acceptance.md +++ b/docs/windows-acceptance.md @@ -20,23 +20,22 @@ dotnet build apps/windows/Miucs.Windows.csproj -c Release | 项目 | 步骤与预期 | 当前状态 | | --- | --- | --- | | 首次启动与退出 | 标准用户解压启动;有托盘、透明画面、无控制台;退出后进程、监听与托盘消失;重复启动不产生第二份实例 | 未执行 | -| 五线谱水波区域 | UI 2 播放有稳定节拍的音乐;水滴落点与 macOS 相同,水花可越过五线谱上下边界,接近画布边缘才渐隐。检查 75%/100%/200% 缩放、深浅背景与五种笔色;静音/隐藏/重连不补播 | 未执行 | -| 托盘猫摇头 | 三种模式均为同一只猫;开启键盘输入后与像素键盘浮窗同向摇头、停下回正;隐藏画面仍可响应。关闭托盘动画/系统界面动画、锁屏和休眠时静止,恢复不补播。检查深浅任务栏、16~64px 缩放、Explorer 重启、反复开关和切换后的句柄稳定性 | 未执行 | +| 托盘猫摇头 | 四个插件均为同一只猫;开启键盘输入后与像素键盘浮窗同向摇头、停下回正;隐藏画面仍可响应。关闭托盘动画/系统界面动画、锁屏和休眠时静止,恢复不补播。检查深浅任务栏、16~64px 缩放、Explorer 重启、反复开关和切换后的句柄稳定性 | 未执行 | | 透明与焦点 | 浅/深桌面均有可读线条;默认点击空白和笔迹均穿透到下面窗口;不抢键盘焦点、Alt-Tab 不出现额外画面窗口 | 未执行 | | 悬浮锁与调整模式 | 悬浮不足 3 秒不显示,离开后重计时;满 3 秒出现手绘小锁,连续开锁/上锁状态正确。锁定时小锁之外可点击下面应用;解锁后拖动移动、右下角等比缩放;托盘状态同步;移动中切换/隐藏不会留下鼠标捕获 | 未执行 | | 多屏与 DPI | 100% / 150% / 200% 混合缩放、负坐标副屏;拖动跨屏、重启、拔除副屏、变更分辨率后窗口完整落入工作区;任务栏不遮挡 | 未执行 | | 刷新与资源 | 60/120/144Hz 显示器运行;多次打开托盘/切换/隐藏显示;无持续句柄、内存或后台线程增长;隐藏后不再绘制 | 未执行 | -| 两种布局 | 飞鸟 400×260、五线谱 800×160;五档缩放等比;两种模式分别记忆位置尺寸,重启恢复上次模式 | 未执行 | +| 插件布局 | 四个插件均使用各自清单尺寸;等比缩放、分别记忆位置尺寸,首次启动和失效选择默认奶牛猫,重启恢复有效选择 | 未执行 | | 配色 | 五种笔色即时生效、隐藏时改色再显示正确;改色不重连输入,不清理轨迹或粒子;重启保持全局颜色 | 未执行 | | 音频基本 | 默认输出播放合成单音/扫频/鼓点,动画响应;不是麦克风输入;静音保持、停止播放不出现虚构音高 | 未执行 | | 音频格式 | 44.1/48/96kHz、单/双声道、整数/浮点格式;不支持格式明确显示失败而非生成假帧 | 未执行 | | 设备与服务恢复 | 播放中切换默认输出、拔插耳机、音频服务重启;自动恢复;手动重连可用;旧事件不补播 | 未执行 | -| 并行输入 | 音乐播放时启用键盘星爆;停止音乐后键盘仍响应;关闭键盘不影响音乐;默认首次启动键盘关闭 | 未执行 | -| 键盘物理布局 | ANSI 主区、左右修饰键、Caps Lock 每次切换、同按和长按/松开;切换中文输入法不改变位置;方向键/数字小键盘不生成错位星爆 | 未执行 | +| 并行输入 | 音乐播放时启用键盘动作;停止音乐后键盘仍响应;关闭键盘不影响音乐;默认首次启动键盘关闭 | 未执行 | +| 键盘物理布局 | ANSI 主区、左右修饰键、Caps Lock 每次切换、同按和长按/松开;切换中文输入法不改变位置;方向键/数字小键盘不生成错位动作 | 未执行 | | 键盘隐私边界 | 仅主动启用后监听;禁用立即停止;不拦截、不注入、不转换为文字;无按键文件/网络传输;UAC 安全桌面、锁屏中不采集 | 未执行 | | 休眠与会话 | 按住键后锁屏、睡眠、唤醒仍锁定、解锁;安全桌面恢复;不出现旧长按或旧节拍;仅在全部暂停原因解除后恢复 | 未执行 | | 切换与隐藏 | 连续快速切换 50 次、隐藏时切换再显示;音频和键盘不中断;恢复只看最新状态,隐藏期间事件不补播 | 未执行 | -| 识别与绘制 | 合成已知音高对照谱位、鸟高度、音名;音量控制缩放而非音高;弱音/噪声不强制输出音高;节拍、星爆、轨迹同时运行 | 未执行 | +| 识别与绘制 | 猫爪、猫头、尾巴、牛蹄、小狗跳跃、平次挥剑与和叶转身按各自规则响应;音频和键盘互不抢占 | 未执行 | | 长时间与故障 | 至少 30 分钟含静音;损坏偏好文件可启动;偏好目录只读时不崩溃且提示;断开设备时退出不挂起 | 未执行 | ## 发布前边界 diff --git a/modules/plugins/CMakeLists.txt b/modules/plugins/CMakeLists.txt index 9e394d5..e7182b5 100644 --- a/modules/plugins/CMakeLists.txt +++ b/modules/plugins/CMakeLists.txt @@ -21,7 +21,11 @@ target_link_libraries(plugin-tests PRIVATE plugin_core) add_executable(plugin-benchmark ../../tests/plugins/benchmark.cpp) target_link_libraries(plugin-benchmark PRIVATE plugin_core) enable_testing() -add_test(NAME plugin-core COMMAND plugin-tests "${CMAKE_CURRENT_SOURCE_DIR}/../../plugins/staff-note") +add_test(NAME plugin-core COMMAND plugin-tests "${CMAKE_CURRENT_SOURCE_DIR}/../../plugins/pixel-keyboard") +add_executable(plugin-exit-validation ../../tests/plugins/exit_validation.cpp) +target_link_libraries(plugin-exit-validation PRIVATE plugin_core) +add_test(NAME plugin-exit-validation COMMAND plugin-exit-validation) +set_tests_properties(plugin-exit-validation PROPERTIES TIMEOUT 10) option(MIUCS_SKIA "Build Skia renderer and host bridge" ON) if(MIUCS_SKIA) set(SKIA_ROOT "${DEPS}/skia-0746a1afaf750159e1a6aef29992980f4e293585" CACHE PATH "Pinned Skia headers") diff --git a/modules/plugins/src/model.cpp b/modules/plugins/src/model.cpp index cf0a9fa..74a5b44 100644 --- a/modules/plugins/src/model.cpp +++ b/modules/plugins/src/model.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace miucs { std::filesystem::path contained(const std::filesystem::path& root, const std::string& relative) { auto rel=std::filesystem::u8path(relative); @@ -61,12 +63,23 @@ Json catalog(const std::vector& roots) { ids.insert(id); auto path=std::filesystem::canonical(f).u8string();m["directory"]=std::string(path.begin(),path.end()); result["plugins"].push_back(m); } catch(const std::exception& e) {result["errors"].push_back({{"directory",f.string()},{"message",e.what()}});} - } return result; + } + auto rank=[](const Json& plugin) { + constexpr std::string_view order[]={"pixel-keyboard","muffin-puppies","niulai-keyboard","heiji-kazuha"}; + const auto& id=plugin.at("id").get_ref(); + return std::find(std::begin(order),std::end(order),id)-std::begin(order); + }; + std::stable_sort(result["plugins"].begin(),result["plugins"].end(),[&](const Json& a,const Json& b){return rank(a)())) throw Error("Invalid node id"); - static const std::set types={"group","path","image","text","rect","ellipse"}; - if(!types.contains(n.at("type").get())) throw Error("Unsupported node type"); + // Client readers can finish packets during asynchronous close at process + // exit. A heap-owning static set may already have been destroyed by then. + // Literal views have no allocation or teardown dependency. + constexpr std::string_view types[]={"group","path","image","text","rect","ellipse"}; + const auto& type=n.at("type").get_ref(); + if(std::find(std::begin(types),std::end(types),type)==std::end(types)) throw Error("Unsupported node type"); validateProperties(n); } void validateProperties(const Json& n) { diff --git a/plugins/heiji-kazuha/ASSETS.md b/plugins/heiji-kazuha/ASSETS.md new file mode 100644 index 0000000..b5cc06c --- /dev/null +++ b/plugins/heiji-kazuha/ASSETS.md @@ -0,0 +1,10 @@ +# 资源与来源 + +- [确认的皮肤概念图](../../docs/concepts/heiji-kazuha/skin-concept.png):在现有像素键盘皮肤上通过内置 imagegen 迭代,采用用户确认的平次/和叶布局及靛蓝、奶油白、浅绿、樱粉配色。用于设计参考,运行时不加载概念图。 +- `assets/heiji-swing.png`:内置 imagegen 生成的 1536×1024 RGBA 木剑动作图集,4 列 × 3 行,共 12 帧。[完整生成提示词](../../docs/concepts/heiji-kazuha/heiji-prompt.txt)。 +- `assets/kazuha-turn.png`:内置 imagegen 生成的 1536×1024 RGBA 转身图集,4 列 × 2 行,共 8 帧。[完整生成提示词](../../docs/concepts/heiji-kazuha/kazuha-prompt.txt)。弃用的第一组转圈图集未打包。 +- 以上 PNG 原样保存,未重新绘制、抠图或缩放。`tools/plugins/register_heiji_kazuha.py` 只读取透明通道,测量姿态边界与脚底锚点,生成 `frames.lua`;[配准记录](../../docs/concepts/heiji-kazuha/registration.json)包含原图 SHA-256、源区域和锚点。运行时按统一像素比例绘制每个角色的帧,足底对齐;不对单帧做独立拉伸。 +- `assets/NotoSans-Regular.ttf`、`assets/OFL.txt` 复制自现有像素键盘插件,字体使用 SIL Open Font License。 +- `keys.lua` 和键盘轮廓沿用项目现有像素键盘布局;输入校验参照麦芬插件。键盘和短暂音符通过 Lua 场景绘制,角色动作来自图集。 + +服部平次和远山和叶为《名侦探柯南》角色,本插件为用户要求的非官方皮肤。生成资产不代表获得原角色的商业授权。 diff --git a/plugins/heiji-kazuha/README.md b/plugins/heiji-kazuha/README.md new file mode 100644 index 0000000..919876b --- /dev/null +++ b/plugins/heiji-kazuha/README.md @@ -0,0 +1,36 @@ +# 平次与和叶 · 像素皮肤 + +![平次随打字挥木剑,和叶随音乐节拍转圈](../../docs/assets/plugins/heiji-kazuha.gif) + +沿用现有像素皮肤的「上方长条角色框+下方完整键盘」,配色为靛蓝外框、奶油白字母键、浅绿功能键和樱粉按键高亮。左侧是服部平次,右侧是穿和服的远山和叶。 + +GIF 由工作台以合成输入生成,循环 12 秒、采样 16 FPS,无声;与[其他皮肤预览](../../docs/plugin-previews.md)使用相同输入、种子和时间线。 + +## 动作 + +- **平次挥剑**:12 帧模型绘制的木剑动作,包括待机、抬剑、劈斩、随势和收剑。物理按键与修饰键触发动作;最近一秒的实际按键频率控制每次挥剑约 0.30~0.72 秒。连续敲击衔接完整动作,停止后收势。系统自动连发的长按事件不重复触发挥剑。 +- **和叶转圈**:8 个方向的模型绘制关键帧,包含正面、左右侧面、背面及斜侧面。可靠音乐节拍驱动转身,两拍一圈;新节拍不把动作重置到第一帧。停止音乐或失去可靠节拍后,完成当前一圈并回到正面。 +- 键盘、音乐两路独立:没有音乐也能挥剑,没有键盘权限也能随音乐转圈。物理按键高亮不依赖输入法,不读取输入文字。 + +角色使用透明图集和最近邻采样。运行时只选择绘制帧并按脚底锚点定位,不旋转整张人物贴图来假装转身。木剑、衣袖和马尾运动来自绘制的姿态。 + +## 安装与预览 + +完整构建会自动包含本插件。也可以将整个 `heiji-kazuha` 文件夹复制到用户插件目录,然后在应用菜单执行「重新扫描插件」,选择「平次与和叶」。 + +- macOS:`~/Library/Application Support/Miucs/Plugins/heiji-kazuha/` +- Windows:`%LOCALAPPDATA%\Miucs\Plugins\heiji-kazuha\` + +```sh +python3 tools/plugins/validate.py plugins/heiji-kazuha +python3 tests/plugins/heiji_kazuha_replay.py +python3 tools/plugins/lab.py serve --plugin plugins/heiji-kazuha +``` + +工作台可暂停、单步和调整打字频率、BPM;也可以测试静音、权限不可用、丢事件和恢复。两路输入使用独立序号及会话,过期事件不补播,长时间卡顿会清除动作积压。画面支持宿主缩放和不透明度,固定主题色不随公共笔色改变。 + +## 验证与资源 + +生产 Lua worker 的合成输入回放覆盖动作节奏、全部关键帧、输入独立性、长按、修饰键、重复/过期事件、静音、重连、卡顿、边界和生命周期;本地结果见[验证记录](../../docs/validation/heiji-kazuha.json)。这不替代 Windows 桌面实机验收。 + +关键帧来源、生成提示词和脚底配准方法见 [ASSETS.md](ASSETS.md)。 diff --git a/plugins/pencil-flight/assets/NotoSans-Regular.ttf b/plugins/heiji-kazuha/assets/NotoSans-Regular.ttf similarity index 100% rename from plugins/pencil-flight/assets/NotoSans-Regular.ttf rename to plugins/heiji-kazuha/assets/NotoSans-Regular.ttf diff --git a/plugins/pencil-flight/assets/OFL.txt b/plugins/heiji-kazuha/assets/OFL.txt similarity index 100% rename from plugins/pencil-flight/assets/OFL.txt rename to plugins/heiji-kazuha/assets/OFL.txt diff --git a/plugins/heiji-kazuha/assets/heiji-swing.png b/plugins/heiji-kazuha/assets/heiji-swing.png new file mode 100644 index 0000000..7be77c9 Binary files /dev/null and b/plugins/heiji-kazuha/assets/heiji-swing.png differ diff --git a/plugins/heiji-kazuha/assets/kazuha-turn.png b/plugins/heiji-kazuha/assets/kazuha-turn.png new file mode 100644 index 0000000..bddfe1a Binary files /dev/null and b/plugins/heiji-kazuha/assets/kazuha-turn.png differ diff --git a/plugins/heiji-kazuha/frames.lua b/plugins/heiji-kazuha/frames.lua new file mode 100644 index 0000000..ebfb62e --- /dev/null +++ b/plugins/heiji-kazuha/frames.lua @@ -0,0 +1,27 @@ +-- Generated by tools/plugins/register_heiji_kazuha.py; original image pixels unchanged. +return { + heiji={source='assets/heiji-swing.png',frames={ + {rect={52,104,268,253},anchor={175.0,251}}, + {rect={462,89,246,268},anchor={151.0,266}}, + {rect={880,25,197,332},anchor={100.5,330}}, + {rect={1253,30,219,327},anchor={105.5,325}}, + {rect={57,419,273,254},anchor={176.0,252}}, + {rect={438,419,296,256},anchor={197.0,254}}, + {rect={813,426,313,248},anchor={211.5,246}}, + {rect={1167,458,321,216},anchor={224.0,214}}, + {rect={63,731,276,252},anchor={170.0,250}}, + {rect={462,722,255,261},anchor={155.5,259}}, + {rect={813,729,270,254},anchor={170.5,252}}, + {rect={1190,729,276,253},anchor={178.0,251}}, + }}, + kazuha={source='assets/kazuha-turn.png',frames={ + {rect={52,52,336,414},anchor={148.5,412}}, + {rect={441,57,315,409},anchor={118.0,407}}, + {rect={820,59,320,407},anchor={109.0,405}}, + {rect={1179,60,345,406},anchor={139.5,404}}, + {rect={51,548,330,400},anchor={184.0,398}}, + {rect={407,545,337,403},anchor={198.0,401}}, + {rect={779,541,317,407},anchor={191.5,405}}, + {rect={1154,537,338,413},anchor={199.0,411}}, + }}, +} diff --git a/plugins/heiji-kazuha/keyboard.lua b/plugins/heiji-kazuha/keyboard.lua new file mode 100644 index 0000000..841fbb8 --- /dev/null +++ b/plugins/heiji-kazuha/keyboard.lua @@ -0,0 +1,102 @@ +local K={keys=miucs.require('keys.lua'),positions={}} +for _,k in ipairs(K.keys) do K.positions[k.key]=k end +local function base(k) return (#k.label>1 or k.key=='space') and '#B9CBB0' or '#F7F1E4' end +local function rect(id,x,y,w,h,fill) + return {type='rect',id=id,x=x,y=y,width=w,height=h,fill=fill,antialias=false} +end +local function polygon(id,points,fill) + local path={};for i,p in ipairs(points)do path[#path+1]={i==1 and "M" or "L",p[1],p[2]} end;path[#path+1]={"Z"} + return {type="path",id=id,path=path,fill=fill,antialias=false} +end +local function box(id,x,y,w,h,color,s) + s=s or 4 + return polygon(id,{{x+s,y},{x+w-s,y},{x+w-s,y+s},{x+w,y+s},{x+w,y+h-s},{x+w-s,y+h-s},{x+w-s,y+h},{x+s,y+h},{x+s,y+h-s},{x,y+h-s},{x,y+s},{x+s,y+s}},color) +end +function K.scene() + local nodes={};local function add(n)nodes[#nodes+1]=n end + for i,x in ipairs({100,660})do add(box("hinge"..i,x,164,40,28,"#202D43"));add(box("hinge-inner"..i,x+5,166,30,22,"#344763",2))end + add(box("screen-outline",24,0,752,172,"#202D43",8));add(box("screen-side",28,4,744,164,"#344763",6));add(box("screen-dark",34,10,732,152,"#2A3A53",4));add(rect("screen-paper",40,14,720,144,"#F7F1E4")) + add(polygon("keyboard-case",{{28,184},{772,184},{800,402},{790,418},{10,418},{0,402}},"#202D43")) + add(polygon("keyboard-side",{{32,188},{768,188},{792,400},{784,412},{16,412},{8,400}},"#2A3A53")) + add(polygon("keyboard-top",{{34,188},{766,188},{786,398},{14,398}},"#344763"));add(rect("keyboard-shine",38,188,724,3,"#687E99")) + for _,k in ipairs(K.keys) do + add(box("key-border-"..k.key,k.x,k.y,k.width,34,"#202D43",3)) + add(box("key-side-"..k.key,k.x+2,k.y+2,k.width-4,30,"#C4BFB2",2)) + local n=box("key-top-"..k.key,3,1,k.width-6,26,base(k),2);n.x=k.x;n.y=k.y;add(n) + add {type="text",id="key-label-"..k.key,x=k.x+k.width/2-#k.label*3,y=k.y+5,text=k.label,font="assets/NotoSans-Regular.ttf",fontSize=#k.label>3 and 10 or 12,fill="#202D43"} + end + return nodes +end +local modifiers={leftShift=1,rightShift=1,leftControl=2,rightControl=2,leftOption=4,rightOption=4,leftCommand=8,rightCommand=8,['function']=32} +function K.reset() + K.state={session=nil,cursor=0,time=0,pressed={},observed={},modHeld={},flash={},lit={},recent={}} +end +K.reset() +function K.update(frame) + local dt=math.max(0,frame.deltaTime or 0) + local input=frame.keyboard + local session=input and input.sessionID + local changed=session~=K.state.session + if changed then K.reset();K.state.session=session end + local s=K.state;s.time=s.time+dt + local now=frame.now or 0 + local sampled=input and input.sampledAt or -1e9 + local live=input and input.state=='running' and now-sampled>=-.05 and now-sampled<=.4 + local lost=input and (input.droppedEventCount or 0)>0 + local through=input and input.sequence or 0 + local hits={count=0,reset=changed or not live or lost or dt>.5} + for key,expiry in pairs(s.flash) do if expiry<=s.time then s.flash[key]=nil end end + while #s.recent>0 and s.recent[1].5 or lost then + s.flash={};s.pressed={};s.observed={};s.modHeld={};s.recent={};s.caps=false + else + local cursor=s.cursor + local function strike(key) + s.flash[key]=s.time+.10;hits.count=hits.count+1 + s.recent[#s.recent+1]=s.time + if #s.recent>32 then table.remove(s.recent,1) end + end + for _,e in ipairs(input.events or {}) do + local seq=e.sequence or 0 + if seq>cursor and seq<=through then + cursor=seq + local age=math.max(now,sampled)-(e.receivedAt or -1e9) + local key=e.key + if K.positions[key] and age>=0 and age<=.25 then + if e.kind=='pressed' and not modifiers[key] and key~='capsLock' then + if not s.observed[key] then strike(key) end;s.observed[key]=true + elseif e.kind=='released' then s.observed[key]=nil;s.flash[key]=nil + elseif e.kind=='modifiersChanged' then + if key=='capsLock' then strike(key) + elseif modifiers[key] then + if e.modifierIsPressed and not s.modHeld[key] then strike(key) end + s.modHeld[key]=e.modifierIsPressed or nil + if e.modifierIsPressed==false then s.flash[key]=nil end + end + end + end + end + end + s.pressed={} + for _,held in ipairs(input.heldKeys or {}) do if K.positions[held.key] then s.pressed[held.key]=true end end + for key in pairs(s.observed) do if not s.pressed[key] then s.observed[key]=nil end end + local bits=input.modifiers or 0 + for key in pairs(s.modHeld) do + if (bits&modifiers[key])~=0 then s.pressed[key]=true else s.modHeld[key]=nil end + end + s.caps=(bits&16)~=0 + end + s.cursor=math.max(s.cursor,through) + for _,k in ipairs(K.keys) do + local lit=not not(s.pressed[k.key] or s.flash[k.key] or (k.key=='capsLock' and s.caps)) + if s.lit[k.key]~=lit then + s.lit[k.key]=lit + miucs.set('key-side-'..k.key,{fill=lit and '#B77D87' or ((#k.label>1 or k.key=='space') and '#8EAA8A' or '#C4BFB2')}) + miucs.set('key-top-'..k.key,{fill=lit and '#E9A6AA' or base(k),y=k.y+(lit and 3 or 0)}) + miucs.set('key-label-'..k.key,{y=k.y+(lit and 8 or 5)}) + end + end + hits.rate=#s.recent + return hits +end +return K diff --git a/plugins/staff-note/keys.lua b/plugins/heiji-kazuha/keys.lua similarity index 100% rename from plugins/staff-note/keys.lua rename to plugins/heiji-kazuha/keys.lua diff --git a/plugins/heiji-kazuha/main.lua b/plugins/heiji-kazuha/main.lua new file mode 100644 index 0000000..3f9520a --- /dev/null +++ b/plugins/heiji-kazuha/main.lua @@ -0,0 +1,20 @@ +local K=miucs.require('keyboard.lua') +local M=miucs.require('motion.lua') +local S=miucs.require('scene.lua') +local active=false +local state=M.new() +local function reset() + K.reset();state=M.new() + K.update({now=0,deltaTime=0});S.update(state) +end +return { + create=function() miucs.load(S.create());reset() end, + activate=function(viewport) active=viewport.id=='heiji-kazuha';reset() end, + update=function(frame) + if active then S.update(M.update(state,frame,K.update(frame))) end + end, + appearanceChanged=function(a) miucs.set('heiji-kazuha',{opacity=a.opacity or 1}) end, + resize=function() end, + deactivate=function() active=false end, + dispose=function() active=false end +} diff --git a/plugins/heiji-kazuha/manifest.json b/plugins/heiji-kazuha/manifest.json new file mode 100644 index 0000000..c6d9ed7 --- /dev/null +++ b/plugins/heiji-kazuha/manifest.json @@ -0,0 +1,9 @@ +{ + "id": "heiji-kazuha", + "name": "平次与和叶", + "version": "1.0.0", + "api": 1, + "entry": "main.lua", + "inputs": ["audio", "keyboard"], + "layout": {"width": 800, "height": 460, "minimumWidth": 80, "maximumWidth": 1600} +} diff --git a/plugins/heiji-kazuha/motion.lua b/plugins/heiji-kazuha/motion.lua new file mode 100644 index 0000000..2baf3e2 --- /dev/null +++ b/plugins/heiji-kazuha/motion.lua @@ -0,0 +1,86 @@ +-- Input-driven frame clocks. No rotation of a flat character image and no +-- unbounded queue: rapid input changes cadence but cannot restart a slash. +local M={} +local function clamp(x,a,b) return math.max(a,math.min(b,x)) end +local weights={.06,.10,.10,.10,.06,.06,.08,.10,.10,.08,.08,.08} +local function sword() return {age=1,duration=.7,pending=0,frame=1} end +local function dancer() return {phase=0,speed=1,target=1,untilTime=0,active=false,frame=1,pulse=1} end +function M.new() + return {time=0,heiji=sword(),kazuha=dancer(),audioSession=nil,audioCursor=0,audioTime=nil} +end +local function swing(s,hits,dt) + if hits.reset then s.heiji=sword() end + local h=s.heiji + h.age=h.age+dt + if hits.count>0 then h.pending=s.time+.22 end + if h.age>=h.duration and h.pending>s.time then + h.duration=clamp(.76-.036*hits.rate,.30,.72) + h.age=0;h.pending=0 + end + h.frame=1 + if h.age.5 then + s.heiji=sword();s.kazuha=dancer();s.audioCursor=math.max(s.audioCursor,through) + return s + end + swing(s,hits,dt) + local age=input.frameAge + local live=input.state=='running' and f.sessionID==input.sessionID and age and age>=0 and age<.4 + and a.isSilent==false and now-(f.receivedAt or -1e9)>=-.05 and now-(f.receivedAt or -1e9)<=.45 + local discontinuity=live and s.audioTime and + (a.times.audioCursor and seq<=through then + s.audioCursor=seq + if lag>=0 and lag<=.25 and (e.confidence or 0)>=.45 and (e.confidence or 2)<=1 + and (e.bpm or 0)>=50 and (e.bpm or 0)<=200 and (e.strength or -1)>=0 and (e.strength or 2)<=1 then + accepted=e + end + end + end + end + s.audioCursor=math.max(s.audioCursor,through) + local d=s.kazuha;d.pulse=d.pulse+dt + if accepted and (input.droppedBeatCount or 0)==0 then + d.target=accepted.bpm/120 -- two beats per complete turn + if not d.active then d.phase=0;d.speed=d.target end + d.active=true;d.untilTime=s.time+1.35*60/accepted.bpm;d.pulse=0 + end + if not live or (input.droppedBeatCount or 0)>0 then d.untilTime=0 end + if d.active then + d.speed=d.speed+(d.target-d.speed)*(1-math.exp(-dt*8)) + local nextPhase=d.phase+dt*d.speed + -- Finish at the next front pose after silence/beat loss; never loop on a + -- stale BPM snapshot. Fresh beats extend the run without resetting phase. + if s.time>d.untilTime and nextPhase>=1 then + d.active=false;d.phase=0;d.frame=1 + else + d.phase=nextPhase%1;d.frame=math.min(8,math.floor(d.phase*8)+1) + end + else d.frame=1 end + return s +end +return M diff --git a/plugins/heiji-kazuha/scene.lua b/plugins/heiji-kazuha/scene.lua new file mode 100644 index 0000000..9b48465 --- /dev/null +++ b/plugins/heiji-kazuha/scene.lua @@ -0,0 +1,37 @@ +local K=miucs.require('keyboard.lua') +local F=miucs.require('frames.lua') +local S={} +local actors={heiji={x=235,y=153,scale=.38},kazuha={x=584,y=153,scale=.285}} +local last={} +local function pose(name,index) + local f=F[name].frames[index];local r=f.rect;local a=actors[name] + return {sourceRect=r,x=a.x-f.anchor[1]*a.scale,y=a.y-f.anchor[2]*a.scale, + width=r[3]*a.scale,height=r[4]*a.scale} +end +function S.create() + last={} + local nodes=K.scene();local stage={} + for _,name in ipairs({'heiji','kazuha'}) do + local n=pose(name,1) + n.type='image';n.id=name;n.source=F[name].source;n.sampling='nearest' + stage[#stage+1]=n + end + for i,position in ipairs({{515,72},{651,106}}) do + stage[#stage+1]={type='group',id='note-'..i,x=position[1],y=position[2],opacity=0,children={ + {type='rect',id='note-stem-'..i,x=5,y=0,width=2,height=11,fill='#D88997',antialias=false}, + {type='rect',id='note-flag-'..i,x=7,y=0,width=4,height=3,fill='#D88997',antialias=false}, + {type='rect',id='note-head-'..i,x=0,y=9,width=6,height=4,fill='#D88997',antialias=false} + }} + end + nodes[#nodes+1]={type='group',id='stage',clip={40,14,720,144},z=5,children=stage} + return {type='group',id='heiji-kazuha',width=800,height=460,children=nodes} +end +function S.update(s) + for _,name in ipairs({'heiji','kazuha'}) do + local index=s[name].frame + if last[name]~=index then miucs.set(name,pose(name,index));last[name]=index end + end + local opacity=math.max(0,1-s.kazuha.pulse/.28) + for i=1,2 do miucs.set('note-'..i,{opacity=opacity}) end +end +return S diff --git a/plugins/muffin-puppies/README.md b/plugins/muffin-puppies/README.md index ef3fab6..5c79a89 100644 --- a/plugins/muffin-puppies/README.md +++ b/plugins/muffin-puppies/README.md @@ -1,10 +1,10 @@ -# 麦芬与线条小狗 +# 麦芬 -![麦芬与线条小狗:工作台合成音乐与打字输入预览](../../docs/assets/plugins/muffin-puppies.gif) +![麦芬:工作台合成音乐与打字输入预览](../../docs/assets/plugins/muffin-puppies.gif) 工作台使用统一合成输入生成的 12 秒循环 GIF,无声。查看[全部插件预览](../../docs/plugin-previews.md)。 -按已确认的第三版概念图实现的 Miucs API v1 独立皮肤,菜单名称为「麦芬与线条小狗」。 +按已确认的第三版概念图实现的 Miucs API v1 独立皮肤,菜单名称为「麦芬」。 - 奶油白、草绿色键盘;屏幕、每个键帽、双爪与角色采用断续手绘轮廓。键盘线条在创建时固定,运行中不随机闪烁。 - 左侧物理键(1–5、Q–T、A–G、Z–B 与左修饰键)让马尔济斯起跳;右侧键让小金毛起跳;空格交替触发。仅真实新按下触发,长按重复不触发。 @@ -15,7 +15,7 @@ ## 安装 -完整复制本文件夹到 `~/Library/Application Support/Miucs/Plugins/muffin-puppies/`(macOS)或 `%LOCALAPPDATA%\Miucs\Plugins\muffin-puppies\`(Windows),菜单「重新扫描插件」后选择「麦芬与线条小狗」。 +完整复制本文件夹到 `~/Library/Application Support/Miucs/Plugins/muffin-puppies/`(macOS)或 `%LOCALAPPDATA%\Miucs\Plugins\muffin-puppies\`(Windows),菜单「重新扫描插件」后选择「麦芬」。 工作区版本也会由现有构建脚本自动打包。与旧皮肤使用不同 ID 和独立的大小、位置偏好。 diff --git a/plugins/muffin-puppies/manifest.json b/plugins/muffin-puppies/manifest.json index ddd2db8..79f8a00 100644 --- a/plugins/muffin-puppies/manifest.json +++ b/plugins/muffin-puppies/manifest.json @@ -1,9 +1,17 @@ { "id": "muffin-puppies", - "name": "麦芬与线条小狗", + "name": "麦芬", "version": "1.1.0", "api": 1, "entry": "main.lua", - "inputs": ["audio", "keyboard"], - "layout": {"width": 800, "height": 460, "minimumWidth": 80, "maximumWidth": 1600} + "inputs": [ + "audio", + "keyboard" + ], + "layout": { + "width": 800, + "height": 460, + "minimumWidth": 80, + "maximumWidth": 1600 + } } diff --git a/plugins/niulai-keyboard/README.md b/plugins/niulai-keyboard/README.md index 7cbd7e4..9aabcb9 100644 --- a/plugins/niulai-keyboard/README.md +++ b/plugins/niulai-keyboard/README.md @@ -14,7 +14,7 @@ Miucs API v1 独立皮肤,同一包适用于启用 Lua 插件运行库的 macO ## 联动 - 可靠音乐 beat 在两刃间切换,经过中立姿态;无 beat 不凭空造拍。 - 打字 pressesPerSecond 控制有限跳跃频率;完整蓄力、腾空、落地后才能再跳。长按重复不增加输入通道的真实打字速度。 -- 键位与双蹄由物理按下/保持/释放控制,与上方运动并行。与像素音乐键盘一致:松键后牛蹄留在最后敲击的键位并抬起,仅新击键移动目标;不会每次回F/J。 +- 键位与双蹄由物理按下/保持/释放控制,与上方运动并行。与奶牛猫一致:松键后牛蹄留在最后敲击的键位并抬起,仅新击键移动目标;不会每次回F/J。 - 音频、键盘断开独立处理;输入无活动时保持中立,不是循环 GIF。 - 所有姿态始终向右,不镜像;腾空时延后换刃直到落地。 - 支持源快照比宿主 frame.now 稍晚(最大50ms),避免原呆呆兽版的时序缺陷。 diff --git a/plugins/pencil-flight/assets/README.md b/plugins/pencil-flight/assets/README.md deleted file mode 100644 index 4b364e8..0000000 --- a/plugins/pencil-flight/assets/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Built-in visual assets - -- `NotoSans-Regular.ttf` is the Noto Sans font from the Noto project (`notofonts/noto-fonts`, hinted/ttf/NotoSans/NotoSans-Regular.ttf). Its SIL Open Font License is in `OFL.txt`. It is bundled to keep both platforms on the same font file. - -Native runtime notices are shipped separately in `third_party/plugin-licenses`. diff --git a/plugins/pencil-flight/common.lua b/plugins/pencil-flight/common.lua deleted file mode 100644 index 9716547..0000000 --- a/plugins/pencil-flight/common.lua +++ /dev/null @@ -1,21 +0,0 @@ -local M={} -function M.clamp(x,a,b) return math.max(a,math.min(b,x)) end -function M.approach(x,target,dt,tau) return x+(target-x)*(1-math.exp(-dt/tau)) end -function M.fresh(event,now,cursor,through,age) - local lag=now-(event.receivedAt or -1e9) - return event.sequence>cursor and event.sequence<=through and lag>=0 and lag<=age -end -function M.audio(input) - local f=input and input.frame - if not f or input.state~="running" or f.sessionID~=input.sessionID or not input.frameAge or input.frameAge<0 or input.frameAge>=.4 or f.audio.isSilent then return nil end - return f.audio,f -end -function M.path(id,path,extra) - local n={id=id,type="path",path=path,stroke="#3d3d3d",strokeWidth=1} - for k,v in pairs(extra or {}) do n[k]=v end - return n -end -function M.rect(id,x,y,w,h,fill) - return {type="rect",id=id,x=x,y=y,width=w,height=h,fill=fill,antialias=false} -end -return M diff --git a/plugins/pencil-flight/flight.lua b/plugins/pencil-flight/flight.lua deleted file mode 100644 index 1975d51..0000000 --- a/plugins/pencil-flight/flight.lua +++ /dev/null @@ -1,72 +0,0 @@ -local C=miucs.require("common.lua") -local W=miucs.require("flight_state.lua") -local M={ink="#3d3d3d",ids={}} -local cloud={{"M",-24,9},{"C",-27,1,-20,-5,-14,-1},{"C",-13,-18,8,-19,9,-4},{"C",23,-11,31,2,25,9},{"C",5,12,-10,7,-24,9}} -local puff={{"M",.8,.3},{"C",.8,.8,.5,1,.2,.8},{"C",-.2,1.1,-.8,1,-.7,.5},{"C",-1.2,.5,-1.3,-.3,-.8,-.4},{"C",-.8,-1,-.1,-1.1,.1,-.6},{"C",.7,-.8,.9,-.2,.6,-.1},{"C",.3,.3,-.4,-.2,-.15,.15}} -local function path(id,p,extra) - local n=C.path(id,p,extra);n.halo=true;n.haloWidth=1.05;n.haloOpacity=.62;M.ids[#M.ids+1]=id;return n -end -local function sketch(points,seed) - local result={{"M",points[1][1],points[1][2]}};local index=0 - for i=2,#points do local a,b=points[i-1],points[i];local steps=math.max(1,math.floor(math.sqrt((b[1]-a[1])^2+(b[2]-a[2])^2)/7)) - for step=1,steps do local t=step/steps;local jitter=step==steps and 0 or W.noise(index,seed)*.27 - result[#result+1]={"L",a[1]+(b[1]-a[1])*t+jitter,a[2]+(b[2]-a[2])*t+jitter*.7};index=index+1 - end - end;return result -end -function M.scene() - local nodes={};local function add(n)nodes[#nodes+1]=n end - for i=0,2 do add(path("flight-cloud-"..i,cloud,{opacity=.36}))end - add(path("flight-ground",sketch({{8,247},{130,247.4},{266,246.8},{391,247}},401),{strokeWidth=.9,opacity=.44})) - for i=0,12 do add(path("flight-grass-"..i,sketch({{0,0},{3+i%3,-1}},i+600),{opacity=.33,strokeWidth=.65}))end - local gates={};for i=1,8 do gates[#gates+1]=path("flight-gate-"..i,{{"M",0,0}},{opacity=0})end - add {type="group",id="flight-gates",z=1,clip={96,0,304,260},mask={from={96,0},to={400,0},stops={{0,"#ffffff00"},{1,"#ffffffff"}}},children=gates} - for i=1,80 do add(path("flight-particle-"..i,{{"M",0,0},{"L",1.6,-.8}},{opacity=0,strokeWidth=.7,z=2}))end - for i=1,6 do add(path("flight-puff-"..i,puff,{opacity=0,strokeWidth=.8,constantStroke=true,z=3}))end - add {type="group",id="flight-bird",x=96,y=126,z=4,children={ - path("flight-body",{{"M",-15,0},{"C",-14,-14,0,-15,7,-10},{"C",14,-9,15,-4,15,1},{"C",16,15,-14,17,-15,0}},{strokeWidth=1.55,opacity=.96}), - path("flight-beak",sketch({{14,-1},{23,2},{14,5}},31),{strokeWidth=1.35}), - path("flight-feathers",sketch({{-14,2},{-23,-1},{-18,7},{-12,8}},32),{strokeWidth=1.3}), - {type="ellipse",id="flight-eye",x=7,y=-5,width=2.8,height=3.1,fill="#3d3d3d",z=2}, - path("flight-wing",{{"M",-7,3},{"C",-15,6,-9,17,1,7}},{strokeWidth=1.4,opacity=.94,z=3}), - path("flight-motion",{{"M",-13,-5},{"L",-17,-8}},{strokeWidth=.7,opacity=0,z=4}) - }} - add {type="text",id="flight-label",x=40,y=114,text="-",font="assets/NotoSans-Regular.ttf",fontSize=17,z=5,opacity=.3} - add {type="text",id="flight-score",x=354,y=9,text="0",font="assets/NotoSans-Regular.ttf",fontSize=21,z=5,opacity=.75} - return {type="group",id="pencil-flight",width=400,height=260,children=nodes} -end -function M.reset()W.reset()end -function M.appearance(color)M.ink=color;for _,id in ipairs(M.ids)do miucs.set(id,{stroke=color})end;for _,id in ipairs({"flight-eye","flight-label","flight-score"})do miucs.set(id,{fill=color})end end -function M.update(frame) - W.update(frame) - for i=0,2 do local raw=i*171+38-W.distance*(.10+i*.025);local x=raw-math.modf(raw/510)*510;if x< -65 then x=x+510 end - miucs.set("flight-cloud-"..i,{x=x,y=34+(i%2)*38,strokeWidth=.85-W.brightness*.22})end - for i=0,12 do miucs.set("flight-grass-"..i,{x=(i*37-W.distance*.65)%440,y=250+math.abs(W.noise(i,9))*5})end - for i=1,8 do local g=W.gates[i];local x=g and 96+g.x-W.distance or 0 - if g and x>96 and x<440 then - local body=17+W.bass*W.flap*1.5;local cap=24+W.bass*W.flap*2;local pathData={} - local function add(points,seed)for _,cmd in ipairs(sketch(points,seed))do pathData[#pathData+1]=cmd end end - for _,side in ipairs({-1,1})do add({{side*body,-3},{side*body,g.top-9}},g.id*11+side+1000);add({{side*body,g.bottom+9},{side*body,246}},g.id*11+side+1100)end - for index,pair in ipairs({{g.top,-1},{g.bottom,1}})do local edge,d=pair[1],pair[2] - add({{-cap,edge+d*9},{-cap,edge},{cap,edge},{cap,edge+d*9},{-cap,edge+d*9}},g.id*17+index-1+1700) - for h=0,3 do local hx=-cap+5+h*5;add({{hx,edge+d*2},{hx+3,edge+d*6}},g.id+h+1500)end - end - miucs.set("flight-gate-"..i,{x=x,path=pathData,strokeWidth=1.15+W.bass*.5,opacity=g.passed and .64 or .87}) - else miucs.set("flight-gate-"..i,{opacity=0})end - end - local stretch=1+W.flap*W.loudness*.06;miucs.set("flight-bird",{y=W.y,rotation=W.angle,scaleX=W.scale/stretch,scaleY=W.scale*stretch}) - local lift=W.flap*(10+W.loudness*9);miucs.set("flight-wing",{path={{"M",-7,3},{"C",-15,6-lift,-9,17-lift,1,7-lift}}}) - miucs.set("flight-motion",{path=sketch({{-13,-5-lift*.35},{-17,-8-lift*.45}},42),opacity=W.flap>.35 and W.flap*.45 or 0}) - for i=1,80 do local p=W.particles[i] - miucs.set("flight-particle-"..i,p and {x=p.x,y=p.y,path=p.feather and {{"M",-4,2},{"C",-5,-5,1,-6,4,-2}} or sketch({{0,0},{1.6,-.8}},p.id),opacity=p.life/p.lifetime*(p.feather and .5 or .62),strokeWidth=p.feather and .8 or .7} or {opacity=0}) - end - for i=1,6 do local p=W.puffs[i] - if p then local t=math.min(p.age/p.lifetime,1);local r=(5+p.strength*10)*(.55+t*.85) - miucs.set("flight-puff-"..i,{x=p.x,y=p.y,scaleX=r,scaleY=r*.72,strokeWidth=.75+p.strength*.25,opacity=math.min(p.age/.05,1)*(1-t)*(.2+p.strength*.65)*math.min(p.strength/.15,1)}) - else miucs.set("flight-puff-"..i,{opacity=0})end - end - local note="-";local opacity=.3 - if W.audio and not W.audio.isSilent and W.audioAge<.4 and W.noteAge<.24 and W.note then local names={"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"};note=names[W.note%12+1]..(math.floor(W.note/12)-1);opacity=.88*C.clamp((.24-W.noteAge)/.12,0,1)end - miucs.set("flight-label",{x=math.max(6,96-(W.halfWidth or 20)-10-#note*9),y=W.y-12,text=note,opacity=opacity});miucs.set("flight-score",{text=tostring(W.score)}) -end -return M diff --git a/plugins/pencil-flight/flight_state.lua b/plugins/pencil-flight/flight_state.lua deleted file mode 100644 index bf1049d..0000000 --- a/plugins/pencil-flight/flight_state.lua +++ /dev/null @@ -1,85 +0,0 @@ -local C=miucs.require("common.lua") -local W={} -local function spring(p,v,target,f,dt)local d=p-target;local c=v+f*d;local e=math.exp(-f*dt);return target+(d+c*dt)*e,(v-f*c*dt)*e end -local function ease(x)x=C.clamp(x,0,1);return x*x*(3-2*x)end -local function softmin(a,b)local h=math.max(10-math.abs(a-b),0)/10;return math.min(a,b)-h*h*2.5 end -function W.noise(index,seed)local n=math.sin((index*73+seed*137)*12.9898)*43758.5453;return (n-math.floor(n))*2-1 end -local function resetInput()W.audio=nil;W.audioAge=1e9;W.lastAudio=nil;W.lastMelody=nil;W.note=nil;W.noteAge=1e9;W.history={};W.lower=48;W.upper=84;W.rangeTime=nil;W.onset=false;W.flux=0 end -function W.reset() - W.distance=0;W.y=126;W.velocity=0;W.target=126;W.angle=0;W.scale=.8;W.activity=0;W.flap=0;W.loudness=0;W.bass=0;W.mids=0;W.highs=0;W.brightness=.5;W.scatter=0;W.elapsed=0;W.lead=250 - W.gates={};W.particles={};W.puffs={};W.score=0;W.nextGate=0;W.gateID=0;W.particleID=0;W.particleBudget=0;W.cooldown=0;W.session=nil;W.sequence=0;W.sampleRate=nil;W.channels=nil;resetInput() -end -W.reset() -local function emit(feather) - local a,b=W.noise(W.particleID,7),W.noise(W.particleID,19);local life=feather and .65 or .9 - W.particles[#W.particles+1]={id=W.particleID,x=82,y=W.y+a*4,vx=-12-math.abs(a)*16,vy=b*(8+W.scatter*35),life=life,lifetime=life,feather=feather};W.particleID=W.particleID+1 -end -function W.update(frame) - local input=frame.audio or {};local f=input.frame - if input.sessionID~=W.session then resetInput();W.session=input.sessionID;W.sequence=0;W.sampleRate=nil;W.channels=nil end - if input.state=="running" and f and input.frameAge and input.frameAge<.4 then - if W.sequence~=f.sequence then - if W.sampleRate~=f.audio.sampleRate or W.channels~=f.audio.channelCount then resetInput();W.sampleRate=f.audio.sampleRate;W.channels=f.audio.channelCount end - local a=f.audio - if a.time~=W.lastAudio then W.lastAudio=a.time;W.audio=a;W.audioAge=input.frameAge end - local m=f.melody - if m then - if W.lastMelody and m.time=.48 and m.pitchHz and m.pitchHz>0 and (not W.rangeTime or m.time>W.rangeTime)then - local midi=69+12*math.log(m.pitchHz/440,2);local dt=C.clamp(m.time-(W.rangeTime or m.time),0,.2);W.rangeTime=m.time - W.history[#W.history+1]={time=m.time,midi=midi};while #W.history>2048 or (#W.history>0 and W.history[1].time=24 and m.time-W.history[1].time>=3 then - local values={};for _,p in ipairs(W.history)do values[#values+1]=p.midi end;table.sort(values) - local lo=values[math.floor((#values-1)*.1)+1]-2;local hi=values[math.floor((#values-1)*.9)+1]+2;local center=(lo+hi)/2;local span=math.max(12,hi-lo);local limit=dt*.7 - W.lower=W.lower+C.clamp(center-span/2-W.lower,-limit,limit);W.upper=W.upper+C.clamp(center+span/2-W.upper,-limit,limit);W.upper=math.max(W.upper,W.lower+12) - end - W.target=196-C.clamp((midi-W.lower)/(W.upper-W.lower),0,1)*140 - if midi>=0 and midi<=127 then if W.noteAge>=.24 or not W.note or math.abs(midi-W.note)>.65 then W.note=math.floor(midi+.5)end;W.noteAge=0 end - end - end - W.sequence=f.sequence - end - if W.audio and not W.audio.isSilent and W.audioAge<.4 then W.onset=#(input.onsets or {})>0;W.flux=math.max(W.flux,C.clamp(((input.spectralFluxPeak or 0)-.04)/.56,0,1)) end - else resetInput() end - local remaining=math.min(frame.deltaTime,.1) - while remaining>1e-10 do - local dt=math.min(remaining,1/120);remaining=remaining-dt - W.elapsed=W.elapsed+dt;W.audioAge=W.audioAge+dt;W.noteAge=W.noteAge+dt - local a=W.audio or {};local active=W.audio and not a.isSilent and W.audioAge<.4 - local function drive(power)return C.clamp((10*math.log(math.max(power or 0,1e-12),10)+60)/45,0,1)end - W.activity=C.approach(W.activity,active and 1 or 0,dt,.28);if W.activity<.002 then W.activity=0 end - W.loudness=C.approach(W.loudness,active and drive((a.envelope or 0)^2)or 0,dt,.10);W.scale=C.approach(W.scale,.8+.6*W.loudness,dt,.12) - for _,pair in ipairs({{"bass","lowEnergy",.1},{"mids","midEnergy",.3},{"highs","highEnergy",.1}})do W[pair[1]]=C.approach(W[pair[1]],active and drive(a[pair[2]])or 0,dt,pair[3])end - W.brightness=C.approach(W.brightness,C.clamp(math.log(math.max(a.centroidHz or 1000,150)/150,2)/6,0,1),dt,1.2);W.scatter=C.approach(W.scatter,a.flatness or 0,dt,.25) - W.lead=C.approach(W.lead,a.bandwidthHz and 200+90*C.clamp(a.bandwidthHz/5000,0,1) or 250,dt,.45) - local speed=(96+30*W.mids)*W.activity;W.distance=W.distance+speed*dt - W.y,W.velocity=spring(W.y,W.velocity,W.target,32,dt);W.angle=C.approach(W.angle,C.clamp(W.velocity*.0015,-.28,.28),dt,.055) - if active and W.distance>=W.nextGate then local x=W.distance+W.lead - if #W.gates==0 or x-W.gates[#W.gates].x>=74 then - local center=72+miucs.random()*108;local half=40+miucs.random()*8 - W.gates[#W.gates+1]={id=W.gateID,x=x,top=center-half,bottom=center+half,rest=center,half=half,center=center,velocity=0,passed=false};W.gateID=W.gateID+1;W.nextGate=W.distance+74+miucs.random()*16 - end - end - W.flap=W.flap*math.exp(-dt/.14);if W.onset then W.onset=false;if active then W.flap=1;emit(true)end end - local stretch=1+W.flap*W.loudness*.06;local halfw=W.scale*(25/stretch+13*stretch*math.abs(math.sin(W.angle)));local halfh=W.scale*(13*stretch+25/stretch*math.abs(math.sin(W.angle))) - W.halfWidth=halfw - for i=#W.gates,1,-1 do local g=W.gates[i];local ahead=g.x-W.distance - if ahead<220 then - local overlap=30+halfw;local clearance=halfh+14;local approach=ease((220-ahead)/130);local influence=approach*(1-ease((-ahead-overlap)/90));local predicted=C.clamp(W.y+W.velocity*.045,56,196) - local target=ahead>=-overlap and g.rest+(predicted-g.rest)*approach or g.center - g.center,g.velocity=spring(g.center,g.velocity,C.clamp(target,g.half+8,238-g.half),14,dt) - local top,bottom=g.center-g.half,g.center+g.half;g.top=top+(softmin(top,W.y-clearance)-top)*influence;g.bottom=bottom+(-softmin(-bottom,-W.y-clearance)-bottom)*influence - end - if not g.passed and g.x+30+halfw=1 then W.particleBudget=W.particleBudget-1;emit(false)end - for i=#W.particles,1,-1 do local p=W.particles[i];p.life=p.life-dt;p.x=p.x+(p.vx-speed*.5)*dt;p.y=p.y+p.vy*dt;if p.life<=0 or p.x<0 then table.remove(W.particles,i)end end - while #W.particles>80 do table.remove(W.particles,1)end - W.cooldown=math.max(W.cooldown-dt,0) - if active and W.flux>0 and W.cooldown==0 then W.puffs[#W.puffs+1]={x=96-23*W.scale,y=W.y+10*W.scale,strength=W.flux,age=0,lifetime=.5+W.flux*.25};W.cooldown=.14+.2*(1-W.flux);if #W.puffs>6 then table.remove(W.puffs,1)end end - W.flux=0 - for i=#W.puffs,1,-1 do local p=W.puffs[i];p.age=p.age+dt;p.x=p.x-(20+p.strength*24+speed*.35)*dt;p.y=p.y-(3+p.strength*4)*dt;if p.age>=p.lifetime or p.x< -20 then table.remove(W.puffs,i)end end - end -end -return W diff --git a/plugins/pencil-flight/main.lua b/plugins/pencil-flight/main.lua deleted file mode 100644 index 756c07e..0000000 --- a/plugins/pencil-flight/main.lua +++ /dev/null @@ -1,14 +0,0 @@ -local visual=miucs.require("flight.lua") -local active=false -return { - create=function() miucs.load(visual.scene()) end, - activate=function() active=true;visual.reset() end, - update=function(frame) if active then visual.update(frame) end end, - appearanceChanged=function(appearance) - visual.appearance(appearance.ink or "#3d3d3d") - miucs.set("pencil-flight",{opacity=appearance.opacity or 1}) - end, - resize=function(viewport) end, - deactivate=function() active=false;visual.reset() end, - dispose=function() active=false;visual.reset() end -} diff --git a/plugins/pencil-flight/manifest.json b/plugins/pencil-flight/manifest.json deleted file mode 100644 index 103c60a..0000000 --- a/plugins/pencil-flight/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1.0.0", - "api": 1, - "entry": "main.lua", - "id": "pencil-flight", - "name": "铅笔飞鸟", - "inputs": [ - "audio", - "keyboard" - ], - "layout": { - "width": 400, - "height": 260, - "minimumWidth": 40, - "maximumWidth": 1000 - } -} diff --git a/plugins/pixel-keyboard/manifest.json b/plugins/pixel-keyboard/manifest.json index 9cb631e..a95cc7e 100644 --- a/plugins/pixel-keyboard/manifest.json +++ b/plugins/pixel-keyboard/manifest.json @@ -3,7 +3,7 @@ "api": 1, "entry": "main.lua", "id": "pixel-keyboard", - "name": "像素音乐键盘", + "name": "奶牛猫", "inputs": [ "audio", "keyboard" diff --git a/plugins/staff-note/assets/NotoSans-Regular.ttf b/plugins/staff-note/assets/NotoSans-Regular.ttf deleted file mode 100644 index d552209..0000000 Binary files a/plugins/staff-note/assets/NotoSans-Regular.ttf and /dev/null differ diff --git a/plugins/staff-note/assets/OFL.txt b/plugins/staff-note/assets/OFL.txt deleted file mode 100644 index c82d72e..0000000 --- a/plugins/staff-note/assets/OFL.txt +++ /dev/null @@ -1,94 +0,0 @@ -Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts) - -This Font Software is licensed under the SIL Open Font License, -Version 1.1. - -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font -creation efforts of academic and linguistic communities, and to -provide a free and open framework in which fonts may be shared and -improved in partnership with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply to -any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software -components as distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, -deleting, or substituting -- in part or in whole -- any of the -components of the Original Version, by changing formats or by porting -the Font Software to a new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, -modify, redistribute, and sell modified and unmodified copies of the -Font Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, in -Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the -corresponding Copyright Holder. This restriction only applies to the -primary font name as presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created using -the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/staff-note/assets/README.md b/plugins/staff-note/assets/README.md deleted file mode 100644 index 4b364e8..0000000 --- a/plugins/staff-note/assets/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Built-in visual assets - -- `NotoSans-Regular.ttf` is the Noto Sans font from the Noto project (`notofonts/noto-fonts`, hinted/ttf/NotoSans/NotoSans-Regular.ttf). Its SIL Open Font License is in `OFL.txt`. It is bundled to keep both platforms on the same font file. - -Native runtime notices are shipped separately in `third_party/plugin-licenses`. diff --git a/plugins/staff-note/common.lua b/plugins/staff-note/common.lua deleted file mode 100644 index 9716547..0000000 --- a/plugins/staff-note/common.lua +++ /dev/null @@ -1,21 +0,0 @@ -local M={} -function M.clamp(x,a,b) return math.max(a,math.min(b,x)) end -function M.approach(x,target,dt,tau) return x+(target-x)*(1-math.exp(-dt/tau)) end -function M.fresh(event,now,cursor,through,age) - local lag=now-(event.receivedAt or -1e9) - return event.sequence>cursor and event.sequence<=through and lag>=0 and lag<=age -end -function M.audio(input) - local f=input and input.frame - if not f or input.state~="running" or f.sessionID~=input.sessionID or not input.frameAge or input.frameAge<0 or input.frameAge>=.4 or f.audio.isSilent then return nil end - return f.audio,f -end -function M.path(id,path,extra) - local n={id=id,type="path",path=path,stroke="#3d3d3d",strokeWidth=1} - for k,v in pairs(extra or {}) do n[k]=v end - return n -end -function M.rect(id,x,y,w,h,fill) - return {type="rect",id=id,x=x,y=y,width=w,height=h,fill=fill,antialias=false} -end -return M diff --git a/plugins/staff-note/main.lua b/plugins/staff-note/main.lua deleted file mode 100644 index c042b34..0000000 --- a/plugins/staff-note/main.lua +++ /dev/null @@ -1,14 +0,0 @@ -local visual=miucs.require("staff.lua") -local active=false -return { - create=function() miucs.load(visual.scene()) end, - activate=function() active=true;visual.reset() end, - update=function(frame) if active then visual.update(frame) end end, - appearanceChanged=function(appearance) - visual.appearance(appearance.ink or "#3d3d3d") - miucs.set("staff-note",{opacity=appearance.opacity or 1}) - end, - resize=function(viewport) end, - deactivate=function() active=false;visual.reset() end, - dispose=function() active=false;visual.reset() end -} diff --git a/plugins/staff-note/manifest.json b/plugins/staff-note/manifest.json deleted file mode 100644 index e8a9df3..0000000 --- a/plugins/staff-note/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1.0.0", - "api": 1, - "entry": "main.lua", - "id": "staff-note", - "name": "五线谱音符", - "inputs": [ - "audio", - "keyboard" - ], - "layout": { - "width": 800, - "height": 160, - "minimumWidth": 80, - "maximumWidth": 1600 - } -} diff --git a/plugins/staff-note/rings.lua b/plugins/staff-note/rings.lua deleted file mode 100644 index 647c11f..0000000 --- a/plugins/staff-note/rings.lua +++ /dev/null @@ -1,51 +0,0 @@ -local C=miucs.require("common.lua") -local R={} -function R.clear(cursor)R.pulses={};R.elapsed=0;R.cursor=cursor or 0;R.previousX=nil;R.rings={} end -R.clear() -function R.update(dt,events,now,through) - R.elapsed=R.elapsed+dt - for i=#R.pulses,1,-1 do if R.elapsed-R.pulses[i].born>=.89 then table.remove(R.pulses,i) end end - for _,e in ipairs(events or {})do - if e.sequence>R.cursor and e.sequence<=through then - R.cursor=e.sequence;local age=now-e.receivedAt - if age>=0 and age<=.15 and e.strength>=0 and e.strength<=1 and e.confidence>=.45 and e.confidence<=1 and e.bpm>=50 and e.bpm<=200 then - if #R.pulses==4 then table.remove(R.pulses,1) end - local x=112+miucs.random()*600 - if R.previousX and math.abs(x-R.previousX)<120 then x=112+(x-112+300)%600 end - R.previousX=x;local y=48+miucs.random()*64;local phase=miucs.random()*2*math.pi - R.pulses[#R.pulses+1]={born=R.elapsed-age,strength=e.strength,x=x,y=y,phase=phase} - end - end - end - R.cursor=math.max(R.cursor,through);R.rings={} - for _,p in ipairs(R.pulses)do - local age=R.elapsed-p.born - for layer=0,1 do - local a=age-layer*.09 - if a>=0 and a<.8 then local t=a/.8 - R.rings[#R.rings+1]={x=p.x,y=p.y,radius=4+(layer==0 and 28 or 20)*(1-(1-t)^3),opacity=math.min(1,a/.035)*(1-t)^1.1*(layer==0 and .73 or .48)*(.85+.15*p.strength),width=layer==0 and 1.05 or .72,rotation=p.phase+layer*.2,kind="main"} - end - end - for i=0,2 do - local a=age-.10-i*.055 - if a>=0 and a<.58 then - local t=a/.58;local expansion=1-(1-t)^3;local travel=.7+.3*expansion - local angle=p.phase*1.31+({.4,2.5,4.55})[i+1];local distance=28+i*6 - R.rings[#R.rings+1]={x=p.x+math.cos(angle)*distance*travel,y=p.y+math.sin(angle)*distance*.8*travel,radius=1.5+(({6.2,8.3,5.3})[i+1]-1.5)*expansion,opacity=math.min(1,a/.06)*.55*(1-t)^1.2*(.85+.15*p.strength),width=.65,rotation=angle,kind="satellite"} - end - end - end -end -function R.arc(kind,band) - local pieces=kind=="main" and {{.05,1.30},{1.88,3.57},{4.15,5.72}} or {{-.65,1.55}} - local intervals=({{{0,.07},{.93,1}},{{.07,.22},{.78,.93}},{{.22,.78}}})[band] - local path={} - for _,piece in ipairs(pieces)do for _,interval in ipairs(intervals)do - for i=0,24 do - local a=piece[1]+(piece[2]-piece[1])*(interval[1]+(interval[2]-interval[1])*i/24) - local r=10*(1+.016*math.sin(3*a));path[#path+1]={i==0 and "M" or "L",math.cos(a)*r,math.sin(a)*r} - end - end end - return path -end -return R diff --git a/plugins/staff-note/staff.lua b/plugins/staff-note/staff.lua deleted file mode 100644 index f90d6dd..0000000 --- a/plugins/staff-note/staff.lua +++ /dev/null @@ -1,99 +0,0 @@ -local C=miucs.require("common.lua") -local State=miucs.require("staff_state.lua") -local Rings=miucs.require("rings.lua") -local Stars=miucs.require("stars.lua") -local M={ink="#3d3d3d",kinds={}} -local function pencil(id,geometry,extra) - local node=C.path(id,geometry,extra);node.halo=true;return node -end -local function stem(down) - local x=down and -4.8 or 4.8;local tip=down and 26 or -26 - return {{"M",x,0},{"Q",x-.14,tip/2,x+.12,tip},{"M",x,tip}, - {"C",x+3,tip+(down and -4 or 4),x+11,tip+(down and -2 or 7),x+8,tip+(down and -10 or 11)}, - {"Q",x+9,tip+(down and -14 or 15),x+6,tip+(down and -17 or 18)}} -end -local upStem,downStem=stem(false),stem(true) -local sharp,hatch={},{} -for _,x in ipairs({-17,-13.4})do sharp[#sharp+1]={"M",x,-7};sharp[#sharp+1]={"L",x-.5,7}end -for _,y in ipairs({-2.8,2.8})do sharp[#sharp+1]={"M",-20,y+1};sharp[#sharp+1]={"L",-10.6,y-1}end -for i=0,3 do local x=-3.8+i*2;hatch[#hatch+1]={"M",x,1.5};hatch[#hatch+1]={"L",x+1.4,-1.6}end -local arcs={main={},satellite={}} -for _,kind in ipairs({"main","satellite"})do for band=1,3 do arcs[kind][band]=Rings.arc(kind,band) end end -local star={};for i=0,9 do local angle=-math.pi/2+i*math.pi/5;local radius=i%2==0 and 1 or .42;star[#star+1]={i==0 and "M" or "L",math.cos(angle)*radius,math.sin(angle)*radius} end;star[#star+1]={"Z"} -function M.scene() - miucs.load {type="path",id="staff-star-geometry",visible=false,path=star} - for _,kind in ipairs({"main","satellite"})do for band=1,3 do - miucs.load {type="path",id="ring-geometry-"..kind.."-"..band,visible=false,path=arcs[kind][band]} - end end - local nodes={};local function add(n)nodes[#nodes+1]=n end - for i=0,4 do add(pencil("staff-line-"..i,{{"M",20,58+i*8},{"C",240,58+i*8+(i%2==0 and .14 or -.14),570,58+i*8-.12,780,58+i*8}},{opacity=.48,strokeWidth=.72}))end - add(pencil("staff-clef",{{"M",41,86},{"C",35,87,31,81,36,78},{"C",42,72,50,77,48,83},{"C",47,95,30,97,29,89},{"C",19,78,34,67,39,61},{"C",46,54,45,46,41,43},{"C",32,45,34,56,36,63},{"C",39,79,43,93,44,103},{"C",47,117,31,115,33,108},{"C",31,104,34,102,36,105}},{opacity=.8,strokeWidth=1.35})) - add {type="group",id="staff-trace-clip",clip={80,0,560,160},mask={from={80,0},to={640,0},stops={{0,"#ffffff00"},{1,"#ffffffff"}}},children={pencil("staff-trace",{{"M",0,0}},{opacity=.52,strokeWidth=1.05})}} - for i=1,112 do add(pencil("staff-star-"..i,nil,{pathRef="staff-star-geometry",opacity=0,z=2,strokeWidth=.17,haloWidth=.25}))end - local rings={} - for i=1,20 do - local children={} - for band=1,3 do - local taper=({.48,.78,1})[band] - children[#children+1]=C.path("ring-halo-"..i.."-"..band,nil,{pathRef="ring-geometry-main-"..band,stroke="#ffffff",strokeWidth=1.9,opacity=taper*.8,z=band*2}) - children[#children+1]=C.path("ring-ink-"..i.."-"..band,nil,{pathRef="ring-geometry-main-"..band,strokeWidth=1.05,opacity=taper,z=band*2+1}) - end - rings[#rings+1]={type="group",id="ring-"..i,opacity=0,children=children} - end - add {type="group",id="staff-rings",clip={0,0,800,160},mask={from={0,0},to={0,160},stops={{0,"#ffffff00"},{.05,"#ffffffff"},{.95,"#ffffffff"},{1,"#ffffff00"}}},children=rings,z=3} - local function head(id,fill,stroke,width) - return {type="ellipse",id=id,x=-5.7,y=-3.8,width=11.4,height=7.6,pivot={.5,.5},rotation=-.32,fill=fill,stroke=stroke,strokeWidth=width} - end - add {type="group",id="staff-marker",x=640,y=74,opacity=0,z=4,children={head("staff-marker-fill","#f5f5f5cc"),head("staff-marker-stroke",nil,M.ink,.65)}} - add {type="group",id="staff-note-body",x=640,y=74,z=5,children={ - {type="group",id="staff-note-sway",children={pencil("staff-note-stem",upStem,{strokeWidth=1.35})}}, - head("staff-note-outline",nil,M.ink,1.5),head("staff-note-head",M.ink), - C.path("staff-note-hatch",hatch,{stroke="#cccccc38",strokeWidth=.35}), - pencil("staff-sharp",sharp,{strokeWidth=1,opacity=0}) - }} - add {type="text",id="staff-label",x=664,y=66,font="assets/NotoSans-Regular.ttf",fontSize=14,text="",fill=M.ink,z=6} - add {type="text",id="staff-placeholder",x=664,y=66,font="assets/NotoSans-Regular.ttf",fontSize=14,text="—",fill=M.ink,z=6,opacity=.4} - return {type="group",id="staff-note",width=800,height=160,children=nodes} -end -function M.reset()State.reset();Stars.clear();Rings.clear()end -function M.appearance(color) - M.ink=color - for i=0,4 do miucs.set("staff-line-"..i,{stroke=color})end - for _,id in ipairs({"staff-clef","staff-trace","staff-note-stem","staff-sharp","staff-note-outline","staff-marker-stroke"})do miucs.set(id,{stroke=color})end - for _,id in ipairs({"staff-note-head","staff-label","staff-placeholder"})do miucs.set(id,{fill=color})end - for i=1,112 do miucs.set("staff-star-"..i,{stroke=color})end - for i=1,20 do for b=1,3 do miucs.set("ring-ink-"..i.."-"..b,{stroke=color})end end -end -function M.update(frame) - local dt=frame.deltaTime;Stars.update(dt,frame.keyboard) - local reset=State.update(dt,frame.audio);local audio,af=C.audio(frame.audio) - if reset then Rings.clear(State.ringClearCursor or 0) end - if audio then Rings.update(dt,frame.audio.beats,af.receivedAt+frame.audio.frameAge,af.sequence) else Rings.clear((frame.audio and frame.audio.frame or {}).sequence or 0) end - miucs.set("staff-trace",{path=State.path()}) - miucs.set("staff-note-body",{y=State.y,scaleX=State.scale,scaleY=State.scale,opacity=State.opacity}) - miucs.set("staff-note-sway",{rotation=State.angle}) - miucs.set("staff-note-stem",{path=State.y<74 and downStem or upStem}) - miucs.set("staff-marker",{y=State.target,opacity=State.pitch and .28+.2*State.labelOpacity or 0}) - local p=State.pitch;local names={"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"} - local labelY=C.clamp(State.target-8,5,139) - miucs.set("staff-label",{y=labelY,text=p and names[p%12+1]..(math.floor(p/12)-1) or "",opacity=State.labelOpacity*.85}) - miucs.set("staff-placeholder",{y=labelY,opacity=(1-State.labelOpacity)*.4}) - local sharp=p and ({[1]=true,[3]=true,[6]=true,[8]=true,[10]=true})[p%12] - miucs.set("staff-sharp",{opacity=sharp and .9 or 0}) - for i=1,112 do local s=Stars.stars[i];miucs.set("staff-star-"..i,s and {x=s.x,y=s.y,scaleX=s.radius,scaleY=s.radius,rotation=s.rotation,opacity=s.opacity} or {opacity=0})end - for i=1,20 do - local r=Rings.rings[i];miucs.set("ring-"..i,r and {x=r.x,y=r.y,scaleX=r.radius/10,scaleY=r.radius/10*.9,rotation=r.rotation,opacity=r.opacity} or {opacity=0}) - if r then - local scale=r.radius/10;local cosine,sine=math.cos(r.rotation),math.sin(r.rotation) - miucs.set("ring-"..i,{matrix={scale*cosine,scale*.9*sine,-scale*sine,scale*.9*cosine,0,0}}) - for band=1,3 do - miucs.set("ring-halo-"..i.."-"..band,{strokeWidth=(r.width+.85)/(r.radius/10)}) - miucs.set("ring-ink-"..i.."-"..band,{strokeWidth=r.width/(r.radius/10)}) - if M.kinds[i]~=r.kind then - miucs.set("ring-halo-"..i.."-"..band,{pathRef="ring-geometry-"..r.kind.."-"..band}) - miucs.set("ring-ink-"..i.."-"..band,{pathRef="ring-geometry-"..r.kind.."-"..band}) - end - end; M.kinds[i]=r.kind end - end -end -return M diff --git a/plugins/staff-note/staff_state.lua b/plugins/staff-note/staff_state.lua deleted file mode 100644 index 83c173b..0000000 --- a/plugins/staff-note/staff_state.lua +++ /dev/null @@ -1,123 +0,0 @@ -local C=miucs.require("common.lua") -local S={} -local function resetPitch() S.reliable=false;S.pending=nil;S.trackTime=nil;S.reference=nil end -local function stopSway()if S.angle*S.angular>=0 then S.angular=0 end end -local function breakTrace()if S.lastTraceTime then S.segment=S.segment+1 end;S.lastTraceTime=nil end -local function resetInput()resetPitch();breakTrace();S.sounding=false;S.audioAge=1e9;S.contrast=.5;S.volumeTime=nil;S.observation=nil;stopSway()end -function S.reset() - S.elapsed=0;S.pitch=nil;S.target=74;S.y=74;S.velocity=0;S.angle=0;S.angular=0;S.direction=-1;S.scale=.94;S.opacity=.22;S.labelOpacity=0 - S.segment=0;S.trace={};S.melodyAge=1e9;S.loudness=0;S.lastTraceTime=nil;resetInput() - S.session=nil;S.sequence=nil;S.lastMelody=nil;S.lastAudio=nil;S.sampleRate=nil;S.channels=nil;S.onsetCursor=0 -end -S.reset() -function S.continuous()local last=S.trace[#S.trace];return S.sounding and S.reliable and S.audioAge<.4 and S.lastTraceTime and last and last.segment==S.segment end -local function pitchY(midi)local degrees={0,0,1,1,2,3,3,4,4,5,5,6};return 74-((math.floor(midi/12)-1)*7+degrees[midi%12+1]-34)*4 end -local function melody(m,age) - if not S.sounding or age<0 or age>=.24 then S.reliable=false;S.pending=nil;S.trackTime=nil;return end - if m then if S.observation and m.time<=S.observation then return end;S.observation=m.time end - if m and (not m.isMelodic or not m.pitchHz) then resetPitch();stopSway();return end - if not m or m.confidence<.48 or not m.pitchHz or m.pitchHz<=0 then S.pending=nil;S.trackTime=nil;return end - local midi=69+12*math.log(m.pitchHz/440,2) - if midi<42.5 or midi>=84.5 then S.pending=nil;S.trackTime=nil;return end - local resolved=S.reference and math.abs(midi-S.reference)<=.65 and S.reference or math.floor(midi+.5) - if S.trackTime and m.time<=S.trackTime then return end - if S.trackTime and m.time-S.trackTime>.075 then S.pending=nil end;S.trackTime=m.time - local distance=S.reference and math.abs(resolved-S.reference) or 0 - if distance>2 then - if S.pending and math.abs(resolved-S.pending.anchor)<=1 then S.pending.frames=S.pending.frames+1 else S.pending={anchor=resolved,since=m.time,frames=1} end - local seconds,frames=.15,4;if distance<=5 then seconds,frames=.10,2 elseif distance<=10 then seconds,frames=.125,3 end - if S.pending.frames=.08-1e-9 then join={started=S.elapsed,duration=.18+math.min(.12,dt*.12)} end - end - S.lastTraceTime=m.time;S.trace[#S.trace+1]={time=S.elapsed-age,y=y,segment=S.segment,join=join} - if #S.trace>512 then table.remove(S.trace,1) end -end -local function volume(a) - if S.volumeTime and a.time<=S.volumeTime then return end - local db=20*math.log(math.max(a.envelope,1e-6),10) - if S.volumeTime then local ease=1-math.exp(-(a.time-S.volumeTime)/3);S.lower=math.min(db,S.lower+(db-S.lower)*ease);S.upper=math.max(db,S.upper+(db-S.upper)*ease) - else S.lower=db;S.upper=db end - S.volumeTime=a.time;S.contrast=C.clamp(.5+(db-(S.lower+S.upper)/2)/math.max(12,S.upper-S.lower),0,1) -end -local function spring(p,v,target,f,dt)local d=p-target;local c=v+f*d;local decay=math.exp(-f*dt);return target+(d+c*dt)*decay,(v-f*c*dt)*decay end -function S.update(dt,input) - input=input or {};S.elapsed=S.elapsed+dt;S.audioAge=S.audioAge+dt;S.melodyAge=S.melodyAge+dt - if S.audioAge>=.4 then S.sounding=false;stopSway() end - if not S.sounding then resetPitch();breakTrace() elseif S.melodyAge>=.24 then S.reliable=false end - for i=#S.trace,1,-1 do if S.elapsed-S.trace[i].time>=8 then table.remove(S.trace,i) end end - if #S.trace==0 then breakTrace() end;if dt>.25 then stopSway() end - local resetRings=false;S.ringClearCursor=0 - if S.session~=input.sessionID then resetInput();S.trace={};S.session=input.sessionID;S.sequence=nil;S.lastMelody=nil;S.lastAudio=nil;S.sampleRate=nil;S.channels=nil;S.onsetCursor=0;resetRings=true end - local frame=input.frame;local age=input.frameAge - if input.state=="running" and frame and frame.sessionID==input.sessionID and age and age>=0 and age<.4 then - local a=frame.audio - if S.sampleRate~=a.sampleRate or S.channels~=a.channelCount or (S.lastAudio and a.timeS.onsetCursor and e.sequence<=frame.sequence then strength=math.max(strength or 0,e.strength) end end - if strength then S.angular=C.clamp(S.angular+S.direction*(300+135*S.loudness+100*C.clamp(strength,0,1))*math.pi/180,-600*math.pi/180,600*math.pi/180);S.direction=-S.direction end - end - S.onsetCursor=math.max(S.onsetCursor,frame.sequence) - else resetInput() end - local seconds=math.min(dt,.1);local count=math.max(1,math.ceil(seconds*120));local step=seconds/count - for _=1,count do - S.y,S.velocity=spring(S.y,S.velocity,S.target,40,step);S.y=C.clamp(S.y,14,146) - if S.sounding and S.reliable and S.audioAge<.4 and S.pitch then - local decay=math.exp(-5*step);local sine,cosine=math.sin(12*step),math.cos(12*step);local p,v=S.angle,S.angular - S.angle=decay*(p*cosine+(v+5*p)/12*sine);S.angular=decay*(v*cosine-(5*v+169*p)/12*sine) - else S.angle,S.angular=spring(S.angle,S.angular,0,30,step) end - local limit=20*math.pi/180 - if S.angle< -limit then S.angle=-limit;S.angular=math.max(0,S.angular) end;if S.angle>limit then S.angle=limit;S.angular=math.min(0,S.angular) end - S.scale=C.approach(S.scale,S.sounding and S.audioAge<.4 and .8+.7*S.contrast or .94,step,.055) - S.opacity=C.approach(S.opacity,S.reliable and .95 or .22,step,.055);S.labelOpacity=C.approach(S.labelOpacity,S.reliable and 1 or 0,step,.055) - end - return resetRings -end -local function smooth(t)return t*t*(3-2*t)end -local function mix(a,b,t)return {a[1]+(b[1]-a[1])*t,a[2]+(b[2]-a[2])*t}end -function S.path() - local path={};local previous,segment;local live=S.continuous() and S.trace[#S.trace] - local offset=live and S.y-live.y or 0 - local function curve(c1,c2,e)path[#path+1]={"C",c1[1],c1[2],c2[1],c2[2],e[1],e[2]}end - for _,point in ipairs(S.trace)do - local x=640-math.max(0,S.elapsed-point.time)*70;local y=point.y - if live and point.segment==live.segment then y=y+offset*smooth(C.clamp(1-(640-x)/10,0,1)) end - local ending={x,y} - if segment~=point.segment then path[#path+1]={"M",x,y} - elseif point.join then - local progress=smooth(C.clamp((S.elapsed-point.join.started)/point.join.duration,0,1));local handle=(x-previous[1])*.45 - local c1={previous[1]+handle,previous[2]};local c2={x-handle,y} - if progress>=1 then curve(c1,c2,ending) - elseif progress<=0 then path[#path+1]={"M",x,y} - else - local t=progress/2;local s=1-t - local l1=mix(previous,c1,t);local l2=mix(l1,mix(c1,c2,t),t);local le=mix(l2,mix(mix(c1,c2,t),mix(c2,ending,t),t),t);curve(l1,l2,le) - local r2=mix(c2,ending,s);local r1=mix(mix(c1,c2,s),r2,s);local rs=mix(mix(mix(previous,c1,s),mix(c1,c2,s),s),r1,s) - path[#path+1]={"M",rs[1],rs[2]};curve(r1,r2,ending) - end - else path[#path+1]={"L",x,y} end - segment=point.segment;previous=ending - end - if live then path[#path+1]={"L",640,S.y} end - return path -end -return S diff --git a/plugins/staff-note/stars.lua b/plugins/staff-note/stars.lua deleted file mode 100644 index a294985..0000000 --- a/plugins/staff-note/stars.lua +++ /dev/null @@ -1,53 +0,0 @@ -local C=miucs.require("common.lua") -local keys={};for _,k in ipairs(miucs.require("keys.lua"))do keys[k.key]=k end -local S={} -function S.clear()S.elapsed=0;S.bursts={};S.held={};S.session=nil;S.cursor=0;S.count=0;S.stars={};S.caps=nil end -S.clear() -local modifiers={leftShift=true,rightShift=true,leftControl=true,rightControl=true,leftOption=true,rightOption=true,leftCommand=true,rightCommand=true,["function"]=true,capsLock=true} -local function emit(key,born,small) - if not keys[key] then return end - if #S.bursts==16 then table.remove(S.bursts,1) end - S.count=S.count+1;local count=small and 4 or 6;local phase=(S.count%17)*.37;local particles={} - for i=0,count-1 do local angle=i*2*math.pi/count+phase - local r=(3+((i*7+S.count%11)%5)*.5)*(small and .76 or 1);local d=(12+((i*11+S.count%13)%17))*(small and .78 or 1) - particles[#particles+1]={radius=r,dx=math.cos(angle)*d,dy=math.sin(angle)*d,rotation=angle*.3,spin=i%2==0 and .7 or -.6} - end - S.bursts[#S.bursts+1]={key=key,born=born,small=small,particles=particles} -end -function S.update(dt,input) - S.elapsed=S.elapsed+dt - for i=#S.bursts,1,-1 do if S.elapsed-S.bursts[i].born>=.65 then table.remove(S.bursts,i) end end - if input then - if S.session~=input.sessionID then S.session=input.sessionID;S.cursor=0;S.caps=nil;S.held={} end - if input.state~="running" then S.held={};S.cursor=input.sequence;S.caps=((input.modifiers or 0)&16)~=0 - else - if (input.droppedEventCount or 0)>0 then S.held={} end - for _,e in ipairs(input.events or {})do - if C.fresh(e,input.sampledAt,S.cursor,input.sequence,.25) and keys[e.key] then - local born=S.elapsed-(input.sampledAt-e.receivedAt) - if e.kind=="pressed" and not modifiers[e.key] and not S.held[e.key] then emit(e.key,born,false);S.held[e.key]={next=born+.3} - elseif e.kind=="released" then S.held[e.key]=nil - elseif e.kind=="modifiersChanged" then - if e.key=="capsLock" then - local locked=e.modifierIsPressed;if locked==nil then locked=((e.modifiers or 0)&16)~=0 end - if S.caps~=locked then emit(e.key,born,false) end;S.caps=locked - elseif modifiers[e.key] and e.modifierIsPressed then emit(e.key,born,false) end - end - end - end - S.cursor=math.max(S.cursor,input.sequence);S.caps=((input.modifiers or 0)&16)~=0 - local held={};for _,k in ipairs(input.heldKeys or {})do if k.key then held[k.key]=true end end - for k in pairs(S.held)do if not held[k] then S.held[k]=nil end end - end - else S.held={} end - local sorted={};for k in pairs(S.held)do sorted[#sorted+1]=k end;table.sort(sorted) - for _,k in ipairs(sorted)do if S.elapsed+1e-9>=S.held[k].next then emit(k,S.elapsed,true);S.held[k].next=S.elapsed+.18 end end - S.stars={} - for _,b in ipairs(S.bursts)do - local age=math.max(0,S.elapsed-b.born);local t=math.min(1,age/.65);local origin=keys[b.key] - if age<.1 then local p=age/.1;S.stars[#S.stars+1]={x=origin.sx,y=origin.sy,radius=(3.5+4.5*(1-(1-p)^3))*(b.small and .75 or 1),rotation=-.12+p*.24,opacity=.95*(1-p)} end - local travel=1-(1-t)^3 - for _,p in ipairs(b.particles)do S.stars[#S.stars+1]={x=origin.sx+p.dx*travel,y=origin.sy+p.dy*travel,radius=p.radius*(.7+.3*math.min(1,age/.035))*(1-t*.15),rotation=p.rotation+p.spin*t,opacity=.9*(1-t)^1.4} end - end -end -return S diff --git a/scripts/check-boundaries.py b/scripts/check-boundaries.py index 0102ef8..b3480c5 100644 --- a/scripts/check-boundaries.py +++ b/scripts/check-boundaries.py @@ -14,7 +14,7 @@ failures.append(f"{path.relative_to(root)}: shared import {imports}") if re.search(native_names, code): failures.append(f"{path.relative_to(root)}: native type in shared source") - if any(path.is_relative_to(root / f"modules/{source}") for source in ("audio", "keyboard")) and re.search(r"\b(?:SoundFlightWorld|Pencil\w+|Visualization\w+)\b", code): + if any(path.is_relative_to(root / f"modules/{source}") for source in ("audio", "keyboard")) and re.search(r"\b(?:Visualization\w+|PluginSession|PluginCanvas)\b", code): failures.append(f"{path.relative_to(root)}: input source depends on visualization") for platform in ("macos", "windows"): diff --git a/scripts/test-plugins.sh b/scripts/test-plugins.sh index d1f3492..bde3d40 100644 --- a/scripts/test-plugins.sh +++ b/scripts/test-plugins.sh @@ -17,3 +17,6 @@ python3 "$root/tests/plugins/gif_test.py" python3 "$root/tools/plugins/validate.py" "$root/plugins/muffin-puppies" python3 "$root/tests/plugins/muffin_replay.py" + +python3 "$root/tools/plugins/validate.py" "$root/plugins/heiji-kazuha" +python3 "$root/tests/plugins/heiji_kazuha_replay.py" diff --git a/tests/platforms/macos/VisualizationAppearanceTests.swift b/tests/platforms/macos/VisualizationAppearanceTests.swift index dfa901a..e447977 100644 --- a/tests/platforms/macos/VisualizationAppearanceTests.swift +++ b/tests/platforms/macos/VisualizationAppearanceTests.swift @@ -51,18 +51,6 @@ enum VisualizationAppearanceTests { check("The global selection survives a new store", VisualizationAppearanceStore(preferences: preferences).selection == .blue) preferences.set("future-unknown-ink", forKey: VisualizationAppearanceStore.preferencesKey) check("Unknown saved ink safely falls back to graphite", VisualizationAppearanceStore(preferences: preferences).selection == .graphite) - preferences.removeObject(forKey: VisualizationAppearanceStore.preferencesKey) - let legacyData = try JSONEncoder().encode(["bird": VisualizationInk.green.color]) - preferences.set(legacyData, forKey: "MiucsPencilPalette.v1") - let migrated = VisualizationAppearanceStore(preferences: preferences) - check("Legacy flight ink migrates to the nearest shared color without losing its original data", migrated.selection == .green && preferences.data(forKey: "MiucsPencilPalette.v1") == legacyData) - migrated.select(.violet) - check("An explicit shared selection takes precedence over a legacy palette", VisualizationAppearanceStore(preferences: preferences).selection == .violet) - preferences.removeObject(forKey: VisualizationAppearanceStore.preferencesKey) - preferences.set(Data("corrupt".utf8), forKey: "MiucsPencilPalette.v1") - check("Corrupt legacy data cannot enter the shared color model", VisualizationAppearanceStore(preferences: preferences).selection == .graphite) - preferences.set(legacyData, forKey: "MiucsPencilPalette.v1") - let store = VisualizationAppearanceStore(preferences: preferences) let transparencyMenu = VisualizationTransparencyMenu(store: store) var transparencyActionsWork = true @@ -91,28 +79,28 @@ enum VisualizationAppearanceTests { let channel = FeatureChannel(now: { 0 }), generation = UUID() channel.reset(sessionID: generation, state: .running) let registry = VisualizationRegistry() - for id in ["pencil-flight", "staff-note", "pixel-keyboard"] { + for id in ["pixel-keyboard", "muffin-puppies", "niulai-keyboard", "heiji-kazuha"] { registry.register(VisualizationDescriptor(id: id, name: id)) { InkProbe(id: id) } } - let panel = VisualizerPanel(source: channel, registry: registry, selectedID: "pencil-flight", preferences: preferences, persistsFrame: false) + let panel = VisualizerPanel(source: channel, registry: registry, selectedID: "pixel-keyboard", preferences: preferences, persistsFrame: false) let binding = store.$selection.sink { panel.visualizationHost.applyAppearance($0.appearance) } let opacityBinding = store.$transparency.sink { panel.visualizationHost.applyTransparency($0) } defer { binding.cancel(); opacityBinding.cancel(); panel.orderOut(nil); panel.visualizationHost.dispose() } let host = panel.visualizationHost var opacitySurvivesSwitch = true store.setTransparency(70) - for id in ["staff-note", "pixel-keyboard", "pencil-flight"] { + for id in ["muffin-puppies", "niulai-keyboard", "heiji-kazuha", "pixel-keyboard"] { _ = panel.selectVisualization(id: id) opacitySurvivesSwitch = opacitySurvivesSwitch && abs(Double(host.activeView!.alphaValue) - 0.3) < 0.001 && host.subviews.filter { $0 !== host.activeView }.allSatisfy { $0.alphaValue == 1 } } - check("All three modes inherit transparency while adjustment overlays remain opaque", opacitySurvivesSwitch && panel.alphaValue == 1) + check("All four modes inherit transparency while adjustment overlays remain opaque", opacitySurvivesSwitch && panel.alphaValue == 1) host.controller.resume() let session = host.controller.current as! InkProbe, initialFrame = panel.frame store.setTransparency(50) store.select(.blue) check("Appearance updates preserve session, geometry and click-through", host.controller.current === session && panel.frame == initialFrame && panel.ignoresMouseEvents && host.activeView?.alphaValue == 0.5 && session.applied == VisualizationInk.blue.appearance) - _ = panel.selectVisualization(id: "staff-note") + _ = panel.selectVisualization(id: "muffin-puppies") let next = host.controller.current as! InkProbe check("Switching applies the shared ink before activation", next.activeInk == VisualizationInk.blue.appearance) panel.orderOut(nil); store.select(.green); store.setTransparency(20) diff --git a/tests/platforms/windows/CoreTests/ShellTests.cs b/tests/platforms/windows/CoreTests/ShellTests.cs index 0f03244..e010490 100644 --- a/tests/platforms/windows/CoreTests/ShellTests.cs +++ b/tests/platforms/windows/CoreTests/ShellTests.cs @@ -21,11 +21,11 @@ private static void Contained(WindowFrame frame, WindowFrame area, double aspect public static void Run() { var monitor = new WindowFrame(0, 0, 1920, 1040); - var flight = WindowPlacement.Fit(new(80, 80, 400, 260), monitor, 400d / 260, 40, 1000); - Check(flight == new WindowFrame(80, 80, 400, 260), "valid flight placement unchanged"); - var staff = WindowPlacement.Fit(new(-9000, 9000, 1600, 320), monitor, 5, 80, 1600); - Contained(staff, monitor, 5); - Check(Near(staff.X, 0) && Near(staff.Y, 720), "off-screen saved placement restored"); + var canvas = WindowPlacement.Fit(new(80, 80, 400, 260), monitor, 400d / 260, 40, 1000); + Check(canvas == new WindowFrame(80, 80, 400, 260), "valid canvas placement unchanged"); + var wide = WindowPlacement.Fit(new(-9000, 9000, 1600, 320), monitor, 5, 80, 1600); + Contained(wide, monitor, 5); + Check(Near(wide.X, 0) && Near(wide.Y, 720), "off-screen saved placement restored"); var small = new WindowFrame(0, 0, 420, 90); var scaled = WindowPlacement.Fit(new(12, 12, 800, 160), small, 5, 80, 1600); Contained(scaled, small, 5); @@ -37,16 +37,16 @@ public static void Run() var invalid = WindowPlacement.Fit(new(double.NaN, 0, double.PositiveInfinity, 0), monitor, 5, 80, 1600); Contained(invalid, monitor, 5); Check(Near(invalid.Width, 80), "corrupt saved geometry falls back to the 10 percent module minimum"); - var tinyFlight = WindowPlacement.Fit(new(80, 80, 40, 26), monitor, 400d / 260, 40, 1000); - Check(tinyFlight == new WindowFrame(80, 80, 40, 26), "10 percent flight scale stays at the module minimum"); - var clampedFlight = WindowPlacement.Fit(new(80, 80, 10, 6.5), monitor, 400d / 260, 40, 1000); - Check(Near(clampedFlight.Width, 40) && Near(clampedFlight.Height, 26), "flight sizes below 10 percent clamp to the module minimum"); - var tinyStaff = WindowPlacement.Fit(new(80, 80, 80, 16), monitor, 5, 80, 1600); - Check(Near(tinyStaff.Width, 80) && Near(tinyStaff.Height, 16), "10 percent staff scale stays at the module minimum"); + var tinyCanvas = WindowPlacement.Fit(new(80, 80, 40, 26), monitor, 400d / 260, 40, 1000); + Check(tinyCanvas == new WindowFrame(80, 80, 40, 26), "10 percent canvas scale stays at the module minimum"); + var clampedCanvas = WindowPlacement.Fit(new(80, 80, 10, 6.5), monitor, 400d / 260, 40, 1000); + Check(Near(clampedCanvas.Width, 40) && Near(clampedCanvas.Height, 26), "canvas sizes below 10 percent clamp to the module minimum"); + var tinyWide = WindowPlacement.Fit(new(80, 80, 80, 16), monitor, 5, 80, 1600); + Check(Near(tinyWide.Width, 80) && Near(tinyWide.Height, 16), "10 percent wide scale stays at the module minimum"); var tinyKeys = WindowPlacement.Fit(new(80, 80, 80, 46), monitor, 800d / 460, 80, 1600); Check(Near(tinyKeys.Width, 80) && Near(tinyKeys.Height, 46), "10 percent pixel keyboard scale stays at the module minimum"); bool rejected = false; - try { WindowPlacement.Fit(flight, new(0, 0, 0, 10), 5, 600, 1600); } + try { WindowPlacement.Fit(canvas, new(0, 0, 0, 10), 5, 600, 1600); } catch (ArgumentException) { rejected = true; } Check(rejected, "invalid work area rejected"); var random = new Random(1977); @@ -67,13 +67,13 @@ public static void Run() Check(defaults.Transparency == 0, "existing installs keep original opacity"); Check(!defaults.KeyboardEnabled, "keyboard capture is opt-in on first launch"); Check(defaults.TrayAnimationEnabled, "tray animation is enabled without enabling capture"); - Check(defaults.Visualization == "pencil-flight" && defaults.Color == "graphite", "stable first-launch defaults"); + Check(defaults.Visualization == "pixel-keyboard" && defaults.Color == "graphite", "stable first-launch defaults"); Check(Preferences.Palette.Count == 5, "five shared colors"); - defaults.Visualization = "staff-note"; defaults.Color = "mist-blue"; defaults.KeyboardEnabled = true; + defaults.Visualization = "muffin-puppies"; defaults.Color = "mist-blue"; defaults.KeyboardEnabled = true; defaults.Transparency = 60; defaults.TrayAnimationEnabled = false; - defaults.Frames["staff-note"] = new(-100, 70, 1000, 200); - defaults.Frames["pencil-flight"] = new(100, 200, 500, 325); + defaults.Frames["muffin-puppies"] = new(-100, 70, 1000, 200); + defaults.Frames["pixel-keyboard"] = new(100, 200, 500, 325); Check(defaults.Save(path), "preferences atomic save succeeds"); Check(!File.Exists(path + ".tmp"), "no temporary preference file remains after save"); var loaded = Preferences.Load(path); @@ -83,10 +83,10 @@ public static void Run() Check(loaded.Transparency == 90, "transparency retains at least ten percent visibility"); loaded.Transparency = -10; Check(loaded.Transparency == 0, "negative transparency is clamped"); - Check(loaded.Visualization == "staff-note" && loaded.Color == "mist-blue" && loaded.KeyboardEnabled, "global selection and explicit keyboard preference round-trip"); - Check(loaded.Frames.Count == 2 && loaded.Frames["staff-note"] == defaults.Frames["staff-note"] && loaded.Frames["pencil-flight"] == defaults.Frames["pencil-flight"], "module positions/sizes remain independent"); + Check(loaded.Visualization == "muffin-puppies" && loaded.Color == "mist-blue" && loaded.KeyboardEnabled, "global selection and explicit keyboard preference round-trip"); + Check(loaded.Frames.Count == 2 && loaded.Frames["muffin-puppies"] == defaults.Frames["muffin-puppies"] && loaded.Frames["pixel-keyboard"] == defaults.Frames["pixel-keyboard"], "module positions/sizes remain independent"); File.WriteAllText(path, "{ not json"); - Check(Preferences.Load(path).Visualization == "pencil-flight", "truncated JSON recovers to defaults"); + Check(Preferences.Load(path).Visualization == "pixel-keyboard", "truncated JSON recovers to defaults"); File.WriteAllText(path, "null"); Check(!Preferences.Load(path).KeyboardEnabled, "null JSON recovers with keyboard disabled"); File.WriteAllText(path, "{\"Transparency\":999,\"Color\":\"mist-blue\"}"); @@ -96,7 +96,7 @@ public static void Run() Check(Preferences.Load(path).Visualization == "unknown", "plugin selection survives preferences load for catalog resolution"); var unknown = Preferences.Load(path); Check(unknown.Visualization == "unknown" && unknown.Color == "graphite" && unknown.Frames.Count == 0, "plugin IDs preserved and null collections sanitized"); - File.WriteAllText(path, "{\"Frames\":{\"staff-note\":{\"X\":0,\"Y\":0,\"Width\":0,\"Height\":1},\"pencil-flight\":null}}"); + File.WriteAllText(path, "{\"Frames\":{\"muffin-puppies\":{\"X\":0,\"Y\":0,\"Width\":0,\"Height\":1},\"pixel-keyboard\":null}}"); Check(Preferences.Load(path).Frames.Count == 0, "invalid individual saved frames removed"); Check(!loaded.Save(directory), "unwritable file target is a recoverable save failure"); } diff --git a/tests/plugins/README.md b/tests/plugins/README.md index 7e3021e..9d46cac 100644 --- a/tests/plugins/README.md +++ b/tests/plugins/README.md @@ -1,7 +1,11 @@ # Plugin regression tests -`replay.py` compares three bundled Lua visualizations over 600 deterministic frames each. `reference.json.gz` stores the numerical output of the pre-removal Swift simulations, captured on 2026-10-30 and checked against the Lua implementations before removal. It includes melody dropouts, silence, a display stall, keyboard recovery, session changes and audio format changes. `trace_reference.py` independently reconstructs trace geometry from those samples. +`replay.py` checks 奶牛猫 (`pixel-keyboard`) against 600 frozen head/tail samples captured from the earlier Swift simulator. The reference retains its original input SHA-256 and exact cat values; outputs belonging to removed plugins have been discarded. It covers silence, a display stall, keyboard recovery, session changes and audio format changes. + +`niulai_replay.py`, `muffin_replay.py` and `heiji_kazuha_replay.py` verify the other three skins. Catalog and macOS host tests require all four named plugins, with 奶牛猫 first, and cover fallback from an unavailable saved selection. The fixture stores a SHA-256 of the canonical input JSON. Changing the input generator must explicitly update and review the reference; the test never accepts current plugin output automatically. For intentional animation changes, inspect the affected frames and update only the expected values with a documented reason. Historical simulations can be recovered from Git history when needed; they are not compiled or shipped. Run `sh scripts/build-plugins.sh`, then `sh scripts/test-plugins.sh`. macOS host selection, worker disposal and reload tests run with `zsh scripts/test-plugin-macos.sh`. Window and appearance tests use isolated preferences and are separate from real capture/device acceptance. + +`plugin-exit-validation` (CTest) holds a reader until process teardown, after any static destructors registered by the first node validation. It then validates all supported node types and checks rejection of an unsupported type. This reproduces the async-close/static-type-table use-after-free without relying on repeated runs or sleeps. Keep node validation independent of destructible process-wide storage. diff --git a/tests/plugins/benchmark.cpp b/tests/plugins/benchmark.cpp index 3f7db65..c1065c2 100644 --- a/tests/plugins/benchmark.cpp +++ b/tests/plugins/benchmark.cpp @@ -5,7 +5,7 @@ int main(int argc,char**argv){try{ if(argc<3)return 2; auto frames=miucs::Json::parse(miucs::readFile(argv[2],32*1024*1024)); miucs::Json reports=miucs::Json::array(); - for(auto id:{"pixel-keyboard","staff-note","pencil-flight","niulai-keyboard","muffin-puppies"}) { + for(auto id:{"pixel-keyboard","muffin-puppies","niulai-keyboard","heiji-kazuha"}) { miucs::Runtime runtime(std::filesystem::path(argv[1])/id);miucs::Scene host;host.apply(runtime.packet(0)); host.apply(runtime.handle({{"op","activate"},{"value",{{"id",id}}},{"ack",host.version}})); std::vector update,apply;size_t bytes=0,memory=0; diff --git a/tests/plugins/core_tests.cpp b/tests/plugins/core_tests.cpp index b0cc99f..4cc9913 100644 --- a/tests/plugins/core_tests.cpp +++ b/tests/plugins/core_tests.cpp @@ -11,7 +11,7 @@ int main(int argc,char**argv){try{ rejects([&]{contained(root,"../builtin/manifest.json");},"traversal"); rejects([&]{contained(root,"/etc/passwd");},"absolute path"); Runtime runtime(root);Scene scene;auto full=runtime.packet(0);scene.apply(full);check(!scene.nodes.empty(),"initial scene"); - auto changed=runtime.handle({{"op","activate"},{"value",{{"id","staff-note"}}},{"ack",scene.version}});scene.apply(changed); + auto changed=runtime.handle({{"op","activate"},{"value",{{"id",m.at("id")}}},{"ack",scene.version}});scene.apply(changed); auto idle=runtime.handle({{"op","update"},{"value",{{"now",1},{"deltaTime",.016}}},{"ack",scene.version}});scene.apply(idle); auto resync=runtime.handle({{"op","sync"}});check(resync["full"]==true,"resync"); auto bytes=encode(resync);check(decode(bytes.data(),bytes.size())==resync,"MessagePack roundtrip"); @@ -44,10 +44,12 @@ int main(int argc,char**argv){try{ check(unchanged.apply(advance)&&unchanged.version==2,"empty delta can advance version"); } rejects([&]{validateNode({{"id","x"},{"type","path"},{"path",{{"C",1,2}}}});},"path arity"); - const std::set builtinIDs={"pencil-flight","staff-note","pixel-keyboard","niulai-keyboard","muffin-puppies"}; + const std::set builtinIDs={"pixel-keyboard","muffin-puppies","niulai-keyboard","heiji-kazuha"}; auto list=catalog({root.parent_path()});std::set actualIDs; for(auto& item:list["plugins"])actualIDs.insert(item.at("id").get()); check(list["errors"].empty()&&actualIDs==builtinIDs,"independent built-in plugins"); + check(list["plugins"][0]["id"]=="pixel-keyboard"&&list["plugins"][0]["name"]=="奶牛猫","featured cat is first and renamed"); + check(list["plugins"][1]["id"]=="muffin-puppies"&&list["plugins"][1]["name"]=="麦芬","Muffin follows the featured cat"); for(auto& item:list["plugins"]) { auto id=item["id"].get();Runtime single(root.parent_path()/id); check(single.scene.nodes.contains(id),"plugin owns its named output"); diff --git a/tests/plugins/exit_validation.cpp b/tests/plugins/exit_validation.cpp new file mode 100644 index 0000000..12ed2ab --- /dev/null +++ b/tests/plugins/exit_validation.cpp @@ -0,0 +1,34 @@ +#include "model.hpp" +#include +#include +#include +#include + +// Closing a client is asynchronous. Its reader may still validate a packet +// during process teardown, after function-local static destructors have run. +static std::atomic exiting{false}; +static std::thread* reader=nullptr; + +int main() { + // Registered BEFORE the first validation, so this runs AFTER any destructors + // registered by validateNode. No sleeps or scheduling luck are required. + if(std::atexit([] { + exiting.store(true); + reader->join(); + delete reader; + })!=0)return 1; + miucs::validateNode({{"id","warmup"},{"type","group"}}); + reader=new std::thread([] { + while(!exiting.load())std::this_thread::yield(); + try { + for(auto type:{"group","path","image","text","rect","ellipse"}) + miucs::validateNode({{"id","late-packet"},{"type",type}}); + bool rejected=false; + try{miucs::validateNode({{"id","bad"},{"type","unsupported"}});} + catch(const miucs::Error&){rejected=true;} + if(!rejected)std::_Exit(1); + std::fputs("PASS scene validation during process teardown\n",stderr); + }catch(const std::exception& e){std::fprintf(stderr,"Exit validation: %s\n",e.what());std::_Exit(1);} + }); + return 0; +} diff --git a/tests/plugins/heiji_kazuha_replay.py b/tests/plugins/heiji_kazuha_replay.py new file mode 100644 index 0000000..a79ab1e --- /dev/null +++ b/tests/plugins/heiji_kazuha_replay.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Replay real worker inputs: cadence, channel independence, expiry and recovery.""" +import copy +import json +from pathlib import Path +import statistics +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / 'tools/plugins')) +from preview import frame +from wire import send, receive + +worker = ROOT / ('build/plugins/Release/miucs-plugin-worker.exe' if sys.platform == 'win32' else 'build/plugins/miucs-plugin-worker') +package = ROOT / 'plugins/heiji-kazuha' +checks = 0 + + +def check(value, label): + global checks + checks += 1 + if not value: + raise AssertionError(label) + + +class Harness: + def __init__(self): + self.proc = subprocess.Popen([str(worker), str(package)], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + self.scene, self.version, self.index, self.timings = {}, 0, 0, [] + self.read() + self.rest = {name: self.pose(name) for name in ('heiji', 'kazuha')} + self.seen = {name: set() for name in self.rest} + self.activate() + + def pose(self, name): + return tuple(self.scene[name]['sourceRect']) + + def moving(self, name): + return self.pose(name) != self.rest[name] + + def read(self): + r = receive(self.proc.stdout) + if not r.get('ok'): + raise RuntimeError(r.get('error', 'worker failed')) + p = r['scene'] + if p['full']: + self.scene = {} + for n in p['nodes']: + self.scene.setdefault(n['id'], {}).update(n) + self.version = p['version'] + if 'updateMs' in r: + self.timings.append(r['updateMs']) + if hasattr(self, 'seen'): + for name in self.rest: + self.seen[name].add(self.pose(name)) + + def post(self, op, value=None): + send(self.proc.stdin, {'op': op, 'value': value or {}, 'ack': self.version}) + self.read() + + def activate(self): + self.post('activate', {'id': 'heiji-kazuha', 'width': 800, 'height': 460, 'scale': 1}) + + def sample(self, key=None, kind='pressed', held=(), beat=False, bpm=120, audio=True): + f = frame(self.index) + self.index += 1 + f['keyboard'].update(events=[], heldKeys=[{'key': k, 'duration': .1} for k in held], + pressesPerSecond=0, sampledAt=f['now']+.002) + if key: + f['keyboard']['events'] = [dict(sequence=f['keyboard']['sequence'], receivedAt=f['now']+.002, + kind=kind, key=key, modifiers=0)] + f['audio']['frame']['audio']['time'] += 5000 + f['audio']['frame']['receivedAt'] = f['now']+.001 + f['audio']['onsets'] = [] + f['audio']['beats'] = [dict(sequence=f['audio']['frame']['sequence'], receivedAt=f['now']+.001, + time=f['now']+5000, bpm=bpm, confidence=.9, strength=.8)] if beat else [] + if not audio: + del f['audio'] + return f + + def tick(self, **kw): + self.post('update', self.sample(**kw)) + + def advance(self, n=60, **kw): + for _ in range(n): + self.tick(**kw) + + def close(self): + if self.proc.poll() is None: + self.proc.kill() + self.proc.wait(timeout=3) + + +h = Harness() +try: + topology = len(h.scene) + h.advance(120, audio=False) + check(not h.moving('heiji') and not h.moving('kazuha'), 'no fake motion without inputs') + h.tick(key='a', held=('a',), audio=False) + h.advance(12, held=('a',), audio=False) + check(h.moving('heiji') and not h.moving('kazuha'), 'typing drives only sword') + check(h.scene['key-top-a']['fill'] == '#E9A6AA', 'held physical key stays pink') + for _ in range(100): + h.tick(key='a', kind='repeated', held=('a',), audio=False) + check(not h.moving('heiji'), 'OS repeat cannot keep swinging') + h.tick(key='a', kind='released', audio=False) + check(h.scene['key-top-a']['fill'] == '#F7F1E4', 'release restores cream') + for key in ('j', 'space', 'returnKey'): + h.activate(); h.tick(key=key, audio=False); h.advance(12, audio=False) + check(h.moving('heiji'), 'all keyboard regions swing: ' + key) + h.activate() + f = h.sample(key='leftShift', audio=False) + f['keyboard']['modifiers'] = 1 + f['keyboard']['events'][0].update(kind='modifiersChanged', modifierIsPressed=True) + h.post('update', f); h.advance(12, audio=False) + check(h.moving('heiji'), 'modifier press swings') + for mutate, label in [ + (lambda f: f['keyboard']['events'][0].update(receivedAt=f['now']-.5), 'stale key'), + (lambda f: f['keyboard'].update(state='secureInput'), 'secure input'), + (lambda f: f['keyboard'].update(droppedEventCount=2), 'dropped queue'), + (lambda f: f['keyboard'].update(sampledAt=f['now']-.5), 'stale snapshot'), + ]: + h.activate(); f = h.sample(key='a', audio=False); mutate(f); h.post('update', f); h.advance(12, audio=False) + check(not h.moving('heiji'), label + ' cannot swing') + h.activate(); f = h.sample(key='a', audio=False); h.post('update', f); h.advance(60, audio=False) + f['now'] = h.sample(audio=False)['now']; f['keyboard']['sampledAt'] = f['now'] + f['keyboard']['events'][0]['receivedAt'] = f['now'] + h.post('update', f); h.advance(12, audio=False) + check(not h.moving('heiji'), 'duplicate press sequence cannot retrigger') + h.activate() + for _ in range(60): + f = h.sample(audio=False); f['keyboard']['pressesPerSecond'] = 200; h.post('update', f) + check(not h.moving('heiji'), 'summary rate without fresh presses invents no swings') + + def swing_count(interval): + h.activate(); count = 0; previous = False + for i in range(360): + h.tick(key='a' if i % interval == 0 else None, audio=False) + moving = h.moving('heiji') + if moving and not previous: + count += 1 + previous = moving + h.advance(90, audio=False) + check(not h.moving('heiji'), 'typing stops without a long animation backlog') + return count + + slow, fast = swing_count(45), swing_count(5) + check(fast > slow, 'frequent presses produce faster complete swings') + h.activate(); h.tick(beat=True); h.advance(15) + check(h.moving('kazuha') and not h.moving('heiji'), 'beat drives only dancer') + h.advance(150) + check(not h.moving('kazuha'), 'one beat cannot create perpetual rotation') + for mutate, label in [ + (lambda f: f['audio']['frame']['audio'].update(isSilent=True), 'silent audio'), + (lambda f: f['audio']['beats'][0].update(confidence=.2), 'unreliable beat'), + (lambda f: f['audio']['beats'][0].update(receivedAt=f['now']-.5), 'stale beat'), + (lambda f: f['audio'].update(frameAge=.8), 'stale audio frame'), + (lambda f: f['audio'].update(droppedBeatCount=3), 'dropped beat queue'), + (lambda f: f['audio']['frame'].update(sessionID='wrong'), 'wrong session'), + ]: + h.activate(); f = h.sample(beat=True); mutate(f); h.post('update', f); h.advance(15) + check(not h.moving('kazuha'), label + ' cannot turn') + h.activate(); f = h.sample(beat=True); h.post('update', f); h.advance(150) + f['now'] = h.sample()['now']; f['audio']['frame']['receivedAt'] = f['now'] + f['audio']['beats'][0]['receivedAt'] = f['now'] + h.post('update', f); h.advance(15) + check(not h.moving('kazuha'), 'duplicate beat cannot restart turn') + + def turn_count(bpm): + h.activate(); count = 0; previous = False + for i in range(360): + h.tick(beat=i % int(3600/bpm) == 0, bpm=bpm) + moving = h.moving('kazuha') + if not moving and previous: + count += 1 + previous = moving + return count + + slow_turns, fast_turns = turn_count(60), turn_count(120) + check(fast_turns >= slow_turns * 2 - 1 and fast_turns > slow_turns, 'turn speed follows beat tempo') + h.activate(); h.tick(beat=True); h.advance(12) + phase = h.pose('kazuha'); h.tick(beat=True) + check(h.pose('kazuha') == phase, 'new beat does not snap dancer to front') + f = h.sample(key='a'); f['audio'].update(state='recovering', frame=None, frameAge=None) + h.post('update', f); h.advance(12, audio=False) + check(h.moving('heiji'), 'audio loss leaves keyboard working') + h.activate(); f = h.sample(beat=True); f['keyboard'].update(state='secureInput') + h.post('update', f); h.advance(12) + check(h.moving('kazuha'), 'keyboard loss leaves music working') + h.activate(); h.tick(key='a'); h.advance(10) + f = h.sample(beat=True); f['audio']['sessionID'] = 'new' + f['audio']['frame'].update(sessionID='new', sequence=1); f['audio']['beats'][0]['sequence'] = 1 + h.post('update', f) + check(h.moving('heiji'), 'new audio session preserves sword motion') + # Keep the new session long enough to see its low-sequence beat advance. + for _ in range(12): + f = h.sample(); f['audio']['sessionID'] = 'new'; f['audio']['frame']['sessionID'] = 'new'; h.post('update', f) + check(h.moving('kazuha'), 'new audio session accepts low sequence') + f = h.sample(key='j', beat=True); f['deltaTime'] = 2; h.post('update', f) + check(not h.moving('heiji') and not h.moving('kazuha'), 'stall resets both without replaying backlog') + h.activate() + for i in range(1800): + h.tick(key='a' if i % 5 == 0 else None, beat=i % 30 == 0) + for name in ('heiji', 'kazuha'): + n = h.scene[name] + check(40 <= n['x'] and n['x']+n['width'] <= 760 and 14 <= n['y'] and n['y']+n['height'] <= 158, + name + ' complete art stays inside display') + check(len(h.scene) == topology, '30 second stress retains fixed scene size') + check(len(h.seen['heiji']) == 12 and len(h.seen['kazuha']) == 8, 'all generated keyframes actually play') + h.post('appearanceChanged', {'opacity': .4, 'ink': '#000000'}) + check(h.scene['heiji-kazuha']['opacity'] == .4 and h.scene['keyboard-top']['fill'] == '#344763', 'host opacity preserves skin palette') + h.post('resize', {'width': 400, 'height': 230, 'scale': 2}) + check(len(h.scene) == topology, 'resize preserves scene') + h.post('deactivate'); previous = copy.deepcopy(h.scene); h.tick(key='a', beat=True) + check(h.scene == previous, 'inactive plugin ignores inputs') + h.activate() + check(not h.moving('heiji') and not h.moving('kazuha'), 'reactivation starts at rest') + h.post('dispose') + report = {'checks': checks, 'nodeCount': topology, 'swordFrames': 12, 'turnFrames': 8, + 'slowFastSwings': [slow, fast], 'turnsAt60And120Bpm': [slow_turns, fast_turns], + 'scriptP95Ms': sorted(h.timings)[int(len(h.timings)*.95)], 'scriptMeanMs': statistics.mean(h.timings), + 'inputs': 'synthetic, production Lua worker', 'nativeDesktopAcceptance': False} + out = ROOT / 'build/heiji-kazuha-preview'; out.mkdir(exist_ok=True) + (out / 'tests.json').write_text(json.dumps(report, indent=2) + '\n') + print(json.dumps(report, indent=2)) +finally: + h.close() diff --git a/tests/plugins/mac_reload.swift b/tests/plugins/mac_reload.swift index abdd771..9fb9d75 100644 --- a/tests/plugins/mac_reload.swift +++ b/tests/plugins/mac_reload.swift @@ -52,9 +52,23 @@ import AppKit print("PASS CPU surface disposal: 10 reload cycles retain no layers or frame contents") // Catalog scans do not create workers; each selection starts only its own package. let host=PluginHost.shared, registry=PluginVisualizations.registry() - let ids=Set(["pencil-flight","staff-note","pixel-keyboard"]) + let ids=Set(["pixel-keyboard","muffin-puppies","niulai-keyboard","heiji-kazuha"]) precondition(ids.isSubset(of:Set(registry.descriptors.map(\.id)))) precondition(host.packages.isEmpty,"Tray must not eagerly launch a plugin") + precondition(registry.descriptors.prefix(4).map(\.id)==["pixel-keyboard","muffin-puppies","niulai-keyboard","heiji-kazuha"]) + precondition(registry.descriptor(id:"pixel-keyboard")?.name=="奶牛猫") + let suite="MiucsCuratedPlugins-"+UUID().uuidString + let preferences=UserDefaults(suiteName:suite)! + defer { preferences.removePersistentDomain(forName:suite) } + for saved in [nil,"removed-plugin","muffin-puppies","pixel-keyboard"] as [String?] { + if let saved { preferences.set(saved,forKey:VisualizerPanel.selectionPreferencesKey) } + else { preferences.removeObject(forKey:VisualizerPanel.selectionPreferencesKey) } + let panel=VisualizerPanel(source:FeatureChannel(),registry:registry,preferences:preferences,persistsFrame:false,monitorsHover:false) + precondition(panel.descriptor.id==(saved=="muffin-puppies" ? "muffin-puppies" : "pixel-keyboard"),"First launch and missing plugins select the cat; valid choices survive") + panel.visualizationHost.dispose() + precondition(host.packages.isEmpty) + } + print("PASS featured cat ordering, renamed catalog and saved-selection fallback") let keys=KeyboardChannel() let item=NSStatusBar.system.statusItem(withLength:NSStatusItem.variableLength) let tray=AppTrayIcon(item:item,source:keys) diff --git a/tests/plugins/reference.json.gz b/tests/plugins/reference.json.gz index ab547ab..38f3921 100644 Binary files a/tests/plugins/reference.json.gz and b/tests/plugins/reference.json.gz differ diff --git a/tests/plugins/replay.py b/tests/plugins/replay.py index eb71a6a..cef37a9 100644 --- a/tests/plugins/replay.py +++ b/tests/plugins/replay.py @@ -4,7 +4,6 @@ ROOT=pathlib.Path(__file__).resolve().parents[2];sys.path.insert(0,str(ROOT/'tools/plugins')) from wire import send,receive from preview import frame -from trace_reference import path as reference_path out=ROOT/'build/plugin-replay';out.mkdir(parents=True,exist_ok=True) frames=[frame(i) for i in range(600)] # Exercise short pitch dropouts, silence, and a long display stall. @@ -21,8 +20,8 @@ reference=fixture['frames'] assert len(reference)==len(frames), 'Reference must cover every input frame' worker=ROOT/('build/plugins/Release/miucs-plugin-worker.exe' if sys.platform=='win32' else 'build/plugins/miucs-plugin-worker') -with subprocess.Popen([str(worker),str(ROOT/'plugins/staff-note')],stdin=subprocess.PIPE,stdout=subprocess.PIPE) as p: - scene={};version=0;updates=[] +with subprocess.Popen([str(worker),str(ROOT/'plugins/pixel-keyboard')],stdin=subprocess.PIPE,stdout=subprocess.PIPE) as p: + scene={};version=0;updates=[];timings=[];largest=0 def response(): global scene,version result=receive(p.stdout) @@ -30,46 +29,17 @@ def response(): if 'updateMs' in result:updates.append(result['updateMs']) packet=result['scene'] if packet['full']:scene={} + for identity in packet.get('removed',[]):scene.pop(identity,None) for node in packet['nodes']:scene.setdefault(node['id'],{}).update(node) version=packet['version'] - response();send(p.stdin,{'op':'activate','value':{'id':'staff-note'},'ack':version});response() - maximum={};timings=[] + response();send(p.stdin,{'op':'activate','value':{'id':'pixel-keyboard'},'ack':version});response() for i,(f,expected) in enumerate(zip(frames,reference)): start=time.perf_counter();send(p.stdin,{'op':'update','value':f,'ack':version});response();timings.append((time.perf_counter()-start)*1000) - actual={'y':scene['staff-note-body']['y'],'scale':scene['staff-note-body']['scaleX'],'opacity':scene['staff-note-body']['opacity'],'sway':scene['staff-note-sway']['rotation']} - for field,value in actual.items(): - delta=abs(value-expected[field]);maximum[field]=max(maximum.get(field,0),delta) - assert delta<1e-8,(i,field,value,expected[field]) - commands=reference_path(expected);actual_path=scene['staff-trace'].get('path',[]) - assert len(commands)==len(actual_path),(i,'trace-command-count',len(commands),len(actual_path)) - for command,actual_command in zip(commands,actual_path): - assert command[0]==actual_command[0],(i,'trace-operation') - assert max([abs(x-y) for x,y in zip(command[1:],actual_command[1:])]+[0])<1e-8,(i,'trace-geometry') - for r,ring in enumerate(expected['rings'],1): - got=scene[f'ring-{r}'] - for field in ['x','y','opacity','rotation']: - delta=abs(got[field]-ring[field]);maximum['ring-'+field]=max(maximum.get('ring-'+field,0),delta) - assert delta<1e-8,(i,r,field,got[field],ring[field]) - assert abs(got['scaleX']*10-ring['radius'])<1e-8 + rect=scene['keyboard-tail']['sourceRect'];index=int(rect[1]/88)*16+int(rect[0]/96) + assert index==expected['tail'],(i,index,expected['tail']) + delta=abs(scene['keyboard-head'].get('rotation',0)-expected['head']);largest=max(largest,delta) + assert delta<1e-8,(i,delta) send(p.stdin,{'op':'dispose','ack':version});response();p.stdin.close();assert p.wait(timeout=2)==0 - report={'frames':len(frames),'maximumAbsoluteError':maximum,'roundTripP95Ms':sorted(timings)[int(len(timings)*.95)],'roundTripMeanMs':statistics.mean(timings),'scriptP95Ms':sorted(updates)[int(len(updates)*.95)],'scriptMaxMs':max(updates)} + report={'plugin':'pixel-keyboard','frames':len(frames),'maximumAbsoluteError':largest,'roundTripP95Ms':sorted(timings)[int(len(timings)*.95)],'roundTripMeanMs':statistics.mean(timings)} (out/'report.json').write_text(json.dumps(report,indent=2));print(json.dumps(report,indent=2)) - -# Isolate each output in a fresh worker so its deterministic RNG begins at the same seed. -for output in ['pixel-keyboard','pencil-flight']: - with subprocess.Popen([str(worker),str(ROOT/'plugins'/output)],stdin=subprocess.PIPE,stdout=subprocess.PIPE) as p: - scene={};version=0;response();send(p.stdin,{'op':'activate','value':{'id':output},'ack':version});response() - largest=0 - for i,(f,expected) in enumerate(zip(frames,reference)): - send(p.stdin,{'op':'update','value':f,'ack':version});response() - if output=='pixel-keyboard': - rect=scene['keyboard-tail']['sourceRect'];index=int(rect[1]/88)*16+int(rect[0]/96) - assert index==expected['tail'],(output,i,index,expected['tail']) - assert abs(scene['keyboard-head'].get('rotation',0)-expected['head'])<1e-8 - else: - for key in ['y','rotation','scaleX','scaleY']: - delta=abs(scene['flight-bird'][key]-expected['flight'][key]);largest=max(largest,delta) - assert delta<1e-8,(output,i,key,scene['flight-bird'][key],expected['flight'][key]) - assert int(scene['flight-score']['text'])==expected['flight']['score'] - send(p.stdin,{'op':'dispose','ack':version});response();p.stdin.close();assert p.wait(timeout=2)==0 - print(output, '600-frame reference passed; max error',largest) + print('奶牛猫 600-frame frozen reference passed') diff --git a/tests/plugins/soak.py b/tests/plugins/soak.py index f1e1ef4..24434f8 100644 --- a/tests/plugins/soak.py +++ b/tests/plugins/soak.py @@ -17,7 +17,7 @@ def rss(): elapsed=time.monotonic()-start # Switch isolated plugins every ten seconds; also replace the worker every five minutes. gen=int(elapsed//300) - mode=['pixel-keyboard','staff-note','pencil-flight'][int(elapsed//10)%3] + mode=['pixel-keyboard','muffin-puppies','niulai-keyboard','heiji-kazuha'][int(elapsed//10)%4] if gen!=generation or mode!=active: if surface:b.lib.miucs_surface_destroy(surface);surface=None if b:b.close() diff --git a/tests/plugins/trace_reference.py b/tests/plugins/trace_reference.py deleted file mode 100644 index b063a17..0000000 --- a/tests/plugins/trace_reference.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Build the documented reveal geometry from frozen trace samples.""" -def path(reference): - points=reference['trace'];result=[];previous=None;segment=None - live=points[-1] if points and reference['continuous'] else None - offset=reference['y']-live['y'] if live else 0 - def mix(a,b,t):return [a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t] - for point in points: - x,y=point['x'],point['y'] - if live and point['segment']==live['segment']: - blend=max(0,min(1,1-(640-x)/10));y+=offset*blend*blend*(3-2*blend) - end=[x,y];progress=point.get('progress') - if segment!=point['segment']:result.append(['M',x,y]) - elif progress is None:result.append(['L',x,y]) - else: - handle=(x-previous[0])*.45;c1=[previous[0]+handle,previous[1]];c2=[x-handle,y] - if progress>=1:result.append(['C',*c1,*c2,*end]) - elif progress<=0:result.append(['M',x,y]) - else: - t=progress/2;s=1-t;l1=mix(previous,c1,t);l2=mix(l1,mix(c1,c2,t),t);le=mix(l2,mix(mix(c1,c2,t),mix(c2,end,t),t),t) - r2=mix(c2,end,s);r1=mix(mix(c1,c2,s),r2,s);rs=mix(mix(mix(previous,c1,s),mix(c1,c2,s),s),r1,s) - result.extend([['C',*l1,*l2,*le],['M',*rs],['C',*r1,*r2,*end]]) - previous=end;segment=point['segment'] - if live:result.append(['L',640,reference['y']]) - return result diff --git a/tests/plugins/win_surface.cpp b/tests/plugins/win_surface.cpp index 87df004..b4e2c80 100644 --- a/tests/plugins/win_surface.cpp +++ b/tests/plugins/win_surface.cpp @@ -12,7 +12,7 @@ int main(int argc,char**argv){ try { auto post=[&](const miucs::Json& m){auto b=miucs::encode(m);if(!miucs_plugin_post(plugin,b.data(),b.size()))throw miucs::Error("post failed");}; auto fixture=miucs::Json::parse(miucs::readFile(argv[3],32*1024*1024)); - for(auto id:{"pixel-keyboard","staff-note","pencil-flight","niulai-keyboard","muffin-puppies"}) { + for(auto id:{"pixel-keyboard","muffin-puppies","niulai-keyboard","heiji-kazuha"}) { auto folder=(std::filesystem::path(argv[2])/id).string(); plugin=miucs_plugin_open(argv[1],folder.c_str());if(!plugin)throw miucs::Error("Cannot open worker"); post({{"op","activate"},{"value",{{"id",id},{"width",800},{"height",460},{"scale",1}}}}); diff --git a/tools/plugins/lab.py b/tools/plugins/lab.py index d15d171..79392d3 100644 --- a/tools/plugins/lab.py +++ b/tools/plugins/lab.py @@ -39,7 +39,7 @@ def main(): path = Path(args.scenario) if not path.is_file(): path = PRESETS / (args.scenario + ".json") - folders = sorted(p.parent for p in (ROOT / "plugins").glob("*/manifest.json")) + folders = sorted((p.parent for p in (ROOT / "plugins").glob("*/manifest.json")), key=lambda p: (p.name != "pixel-keyboard", p.name)) selected = None if args.plugin: folder = args.plugin.resolve() diff --git a/tools/plugins/lab/scenarios/switching.json b/tools/plugins/lab/scenarios/switching.json index fd925a5..e64b739 100644 --- a/tools/plugins/lab/scenarios/switching.json +++ b/tools/plugins/lab/scenarios/switching.json @@ -9,7 +9,7 @@ { "at": 2, "type": "switch", - "value": "staff-note" + "value": "muffin-puppies" }, { "at": 4, @@ -19,7 +19,7 @@ { "at": 6, "type": "switch", - "value": "pencil-flight" + "value": "heiji-kazuha" }, { "at": 8, diff --git a/tools/plugins/register_heiji_kazuha.py b/tools/plugins/register_heiji_kazuha.py new file mode 100644 index 0000000..8b73bac --- /dev/null +++ b/tools/plugins/register_heiji_kazuha.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Measure generated sprite sheets and write source rectangles, without editing pixels. + +Source cells have irregular spacing. Align each frame at its planted feet while +retaining the same pixel scale across all poses. Registration is a build-time +step; Pillow is not a runtime dependency. The original RGBA images stay intact. +""" +import hashlib +import json +from pathlib import Path +from PIL import Image + +ROOT = Path(__file__).resolve().parents[2] +PACKAGE = ROOT / 'plugins/heiji-kazuha' +SHEETS = { + 'heiji': ('heiji-swing.png', [0, 384, 704, 1024]), + 'kazuha': ('kazuha-turn.png', [0, 512, 1024]), +} + + +def register(): + report = {} + for name, (filename, rows) in SHEETS.items(): + path = PACKAGE / 'assets' / filename + im = Image.open(path) + assert im.mode == 'RGBA' and im.size == (1536, 1024) + alpha = im.getchannel('A') + frames = [] + for top, bottom in zip(rows, rows[1:]): + band = alpha.crop((0, top, 1536, bottom)).point(lambda a: 255 if a > 96 else 0) + cuts = [0] + for expected in (384, 768, 1152): + empty = [x for x in range(expected - 55, expected + 56) + if band.crop((x, 0, x + 1, bottom - top)).getbbox() is None] + assert empty, (name, top, expected, 'no transparent gutter') + cuts.append((min(empty) + max(empty)) // 2) + cuts.append(1536) + for col in range(4): + left, right = cuts[col], cuts[col + 1] + # Ignore near-transparent image-generation matte noise. This + # mask is used only for measurement, never written to an image. + mask = alpha.crop((left, top, right, bottom)).point(lambda a: 255 if a > 96 else 0) + bbox = mask.getbbox() + assert bbox, (name, len(frames)) + x0, y0, x1, y1 = bbox + assert x0 > 2 and y0 > 2 and x1 < right - left - 2 and y1 < bottom - top - 2, (name, bbox) + feet = mask.crop((0, y1 - 14, right - left, y1)).getbbox() + anchor_x = (feet[0] + feet[2]) / 2 + rect = [left + x0 - 2, top + y0 - 2, x1 - x0 + 4, y1 - y0 + 4] + frames.append({'rect': rect, 'anchor': [anchor_x - x0 + 2, y1 - y0 + 2]}) + report[name] = {'source': 'assets/' + filename, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest(), 'frames': frames} + lines = ['-- Generated by tools/plugins/register_heiji_kazuha.py; original image pixels unchanged.', 'return {'] + for name, data in report.items(): + lines.append(f" {name}={{source='{data['source']}',frames={{") + for f in data['frames']: + lines.append(' {rect={' + ','.join(map(str, f['rect'])) + '},anchor={' + ','.join(map(str, f['anchor'])) + '}},') + lines.append(' }},') + lines.append('}') + (PACKAGE / 'frames.lua').write_text('\n'.join(lines) + '\n') + (ROOT / 'docs/concepts/heiji-kazuha/registration.json').write_text(json.dumps(report, indent=2) + '\n') + print('Registered 12 sword poses and 8 turn poses; no image pixels changed') + + +if __name__ == '__main__': + register()